Back to help

REST API

All routes are same-origin under /api. Auth is the same session cookie the web app uses. Responses are always { ok: true, data } or { ok: false, error }.

Auth column key — Any: any signed-in user · Admin: role=admin required (403 for members) · Own record: scoped to the requester, admins can widen with query params · Public (signed): no session, verified by signature/webhook secret instead.

Auth & onboarding

GET
/api/auth/[...all]Better Auth handler — sign in, sign up, session, social callbacks.
Public (signed)
POST
/api/onboarding/create-orgCreate an org for a user who signed up but has none yet. Promotes them to admin.
Any

Clients

GET
/api/clientsList active clients in your org.
Any
POST
/api/clientsCreate a client.
Any
GET
/api/clients/:idGet one client.
Any
PATCH
/api/clients/:idUpdate a client (name, contact, invoice defaults).
Any
DELETE
/api/clients/:idArchive a client (soft delete).
Any

Projects

GET
/api/projectsList projects. Members only see projects they're assigned to; $ budget hidden for members.
Any
POST
/api/projectsCreate a project.
Any
GET
/api/projects/:idGet one project.
Any
PATCH
/api/projects/:idUpdate a project.
Any
DELETE
/api/projects/:idArchive a project.
Any
GET
/api/projects/:id/membersList people assigned to a project.
Any
POST
/api/projects/:id/membersAssign a person to a project.
Any
DELETE
/api/projects/:id/members/:userIdRemove a person from a project.
Any
GET
/api/projects/:id/tasksList tasks assigned to a project.
Any

Tasks

GET
/api/tasksList org tasks.
Any
POST
/api/tasksCreate a task.
Any
PATCH
/api/tasks/:idUpdate a task.
Any
DELETE
/api/tasks/:idArchive a task.
Any

Time & timer

GET
/api/timeList your time entries. ?userId= (admin) views another person's. ?unbilled=true&clientId= (admin) spans the whole org for invoice drafting.
Own record
POST
/api/timeLog a time entry. Requires project assignment.
Any
PATCH
/api/time/:idUpdate an entry. Fails if locked by an invoice.
Own record
DELETE
/api/time/:idHard-delete an unlocked entry and write an audit row. 409 if locked by a Xero push.
Own record
GET
/api/timerGet your currently running timer, or null.
Own record
POST
/api/timer/startStart a timer. Stops any existing running timer first.
Any
POST
/api/timer/stopStop your running timer.
Own record

Expenses

GET
/api/expensesList your expenses. Same admin-only org-wide flags as /api/time.
Own record
POST
/api/expensesLog an expense.
Any
PATCH
/api/expenses/:idUpdate an expense. Fails if locked.
Own record
DELETE
/api/expenses/:idHard-delete an unlocked expense (and its R2 receipt) and write an audit row. 409 if locked by a Xero push.
Own record

Receipts

POST
/api/receipts/upload-urlGet a presigned R2 upload URL for a project you can access. Size/type/ext checked. 503 if R2 isn't fully configured (fail closed).
Any

Rates (money)

GET
/api/ratesList rates (billable + cost). Admin-only — members never see rates, not even their own.
Admin
POST
/api/ratesCreate a rate: default, client override, or project override; billable or cost.
Admin
PATCH
/api/rates/:idUpdate a rate's amount.
Admin
DELETE
/api/rates/:idDelete a rate.
Admin

Budgets

GET
/api/budgets/alertsList budget alert thresholds, optionally filtered by project.
Admin
POST
/api/budgets/alertsAdd an alert threshold (any %, multiple allowed).
Admin
DELETE
/api/budgets/alerts/:idRemove an alert threshold.
Admin

Invoices (admin only, money)

GET
/api/invoicesList invoices.
Admin
POST
/api/invoicesCreate a draft from selected unbilled time entries + expenses. Attaches them; does not lock.
Admin
GET
/api/invoices/:idGet an invoice with its line items, including Xero status after webhook sync.
Admin
DELETE
/api/invoices/:idDelete a draft. Unlinks and unlocks its time entries and expenses.
Admin
POST
/api/invoices/:id/push-xeroPush a draft to Xero and lock its time/expenses. 400 if Xero env is unset.
Admin

Reports (money admin-only)

GET
/api/reportsBreakdown by project/client/person/task, org-wide hours for everyone. Admins additionally get revenue/cost/profit/margin.
Any
GET
/api/reports/uninvoicedUnbilled time + expense value grouped by client, last 6 months.
Admin

Team & settings

GET
/api/usersList org members.
Any
GET
/api/users/:idGet one user.
Any
PATCH
/api/users/:idUpdate name/capacity (self) or role (admin only, any user).
Own record
DELETE
/api/users/:idArchive a team member. Cannot archive yourself.
Admin
GET
/api/settings/orgGet org settings: name, currency, formats, tax rate. Not billing money — same info the Settings page already shows every member (read-only).
Any
PATCH
/api/settings/orgUpdate org settings: currency, formats, fiscal year, tax rate.
Admin

Activity (admin only)

GET
/api/auditOrg activity log. Filter with ?entity=&actor=&from=&to=. Members get 403 and never receive $ fields in before/after.
Admin

Integrations

GET
/api/xero/connectRedirects to Xero's OAuth consent screen. 400 if XERO_CLIENT_ID isn't set.
Admin
DELETE
/api/xero/connectDisconnect Xero. Tokens are deleted and not written to the audit log.
Admin
GET
/api/xero/callbackOAuth callback — exchanges the code, stores the connection.
Admin
POST
/api/xero/webhookXero invoice events (HMAC). Unsigned/forged → 401. Unset signing secret → 503 fail closed. Unlock-on-void never runs without a valid signature.
Public (signed)
GET
/api/harvest/importLatest import job: idle|queued|running|failed|succeeded plus phase/counts. Fresh jobs stay queued/running; leftover dead runs become failed.
Admin
POST
/api/harvest/importPersist queued then running and start a durable Harvest import (resumes the last saved page). 409 if one is already queued/running. 400 if Harvest env isn't set.
Admin

Example

curl https://realtime.fyi/api/timer \
  -H "Cookie: better-auth.session_token=<token from your browser>"

Copy your session token from the browser: DevTools → Application → Cookies → better-auth.session_token.