SDK Reference

Payroll

The Staff class itself — granting a Member staff access via a Role — is documented in Company. This page covers what attaches to a Staff member: pay rates, bonuses, clock-in time tracking (company/staff/), and the pay-period/run layer that batches all of that into paychecks (company/payroll/).

StaffRate

company/staff/rate.ts. A Staffmember's pay rate. payrollScheduleID determines which PayrollSchedulethis rate's hours/bonuses get batched into when a run is processed — the pay-period recurrence itself lives on that schedule, not here.

FieldTypeNotes
id, staffID, companyIDstring
payrollScheduleIDstringrequired
ratenumber
rateUnitStaffRateUnit = "minute" | "hourly" | "daily" | "falt_rate""falt_rate" is a backend typo, kept as-is since it's the literal wire value — not a documentation typo, don't "fix" it client-side
overtimeRatenumberfull data only
overtimeOvernumberfull data only

StaffRateUnitOptions — display labels: "Per Minute", "Hourly", "Daily", "Flat Rate" (the label is spelled correctly even though the underlying value is "falt_rate").

ts
static create(company: Company, rates: CreateStaffRateData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffRate[]>>

CreateStaffRateData = { staffID: string; payrollScheduleID: string; rate?; rateUnit?; overtimeRate?; overtimeOver? }. The backend enforces one rate per (staffID, companyID).

Endpoint: /company/{cID}/staff/rates. viewHref: /company/{cID}/staff/rates/{id}.

Related-entity getters, each cached in a private field:

ts
rate.staff           // Staff | undefined, from staffID
rate.payrollSchedule  // PayrollSchedule | undefined, from payrollScheduleID

Parent → children convenience wrapper:

ts
rate.bonuses(opts: QueryRequest) // StaffPayrollBonus.query filtered by rateID

StaffPayrollBonus

company/staff/payrollBonus.ts. A bonus tied to a StaffRate — added into a PayrollRunLine's bonusAmountwhenever that rate's schedule is processed.

FieldTypeNotes
id, rateID, companyIDstring
bonusTypeStaffPayrollBonusType = "per_class" | "per_service" | "per_class_attendee" | "per_class_attendee_over_amount"
amountnumber

StaffPayrollBonusTypeOptions — display labels: "Per Class", "Per Service", "Per Class Attendee", "Per Class Attendee Over Amount".

ts
static create(company: Company, bonuses: CreateStaffPayrollBonusData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffPayrollBonus[]>>

Static creator is create (lowercase), matching every other class in this doc. The backend enforces one bonus per (rateID, companyID).

Endpoint: /company/{cID}/staff/rates/bonuses. viewHref: /company/{cID}/staff/bonuses/{id}.

ts
bonus.rate // StaffRate | undefined, from rateID — cached in a private field

StaffTimeBlock

company/staff/timeBlock.ts. A staff clock-in/clock-out record. Open (not yet clocked out) while endedAt is unset.

FieldTypeNotes
id, companyID, staffIDstring
locationIDstring | null?optional — not every clock-in needs to be tied to a location
startedAtDate
endedAtDate | null?unset means the block is still open
ts
static create(company: Company, blocks: CreateStaffTimeBlockData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<StaffTimeBlock[]>>

Endpoint: /company/{cID}/staff/timeBlocks. viewHref: /company/{cID}/staff/timeBlocks/{id}.

ts
timeBlock.IsActive: boolean // !this._data?.endedAt
ts
async clockOut(): Promise<SimpleResult<EventData>>
Gotcha:Note the method is clockOut(), lowercase-c (not ClockOut() as its counterpart on the display layer is sometimes referred to) — sets endedAt to new Date() via setData({ ...this._data, endedAt: new Date() }) and calls save().

Related-entity getters, each cached in a private field:

ts
timeBlock.staff     // Staff | undefined, from staffID
timeBlock.location  // Location | undefined, from locationID

Parent → children convenience wrapper:

ts
timeBlock.breaks(opts: QueryRequest) // TimeBlockBreak.query filtered by timeBlockID

TimeBlockBreak

company/staff/timeBlockBreak.ts. A break within a StaffTimeBlock. Mirrors StaffTimeBlock's own active/clock-out pattern via EndBreak().

FieldTypeNotes
id, companyID, timeBlockIDstring
locationIDstring | null?
startedAtDate
endedAtDate | null?unset means the break is still open
ts
static create(company: Company, breaks: CreateTimeBlockBreakData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<TimeBlockBreak[]>>

Endpoint: /company/{cID}/staff/timeBlocks/breaks. viewHref: /company/{cID}/staff/timeBlockBreaks/{id}.

ts
timeBlockBreak.IsActive: boolean // !this._data?.endedAt
async EndBreak(): Promise<SimpleResult<EventData>> // sets endedAt to now, saves — mirrors StaffTimeBlock.clockOut()

Related-entity getters, each cached in a private field:

ts
timeBlockBreak.timeBlock // StaffTimeBlock | undefined, from timeBlockID
timeBlockBreak.location  // Location | undefined, from locationID

PayrollSchedule

company/payroll/schedule.ts. The recurring pay period that StaffRates attach to (via payrollScheduleID) and PayrollRuns are processed against.

FieldTypeNotes
id, companyIDstring
namestring
everyAmountnumber
everyUnitBillingUnitreuses BillingUnit from Feature ("day" | "week" | "month" | "year") — the same recurrence domain used for pricing/resolve-after fields, now living here instead of on each StaffRate
ts
static create(company: Company, schedules: CreatePayrollScheduleData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollSchedule[]>>

CreatePayrollScheduleData = { name: string; everyAmount?; everyUnit? }.

Endpoint: /company/{cID}/payroll/schedules. Has icon (lucide Calendar) and viewHref: /company/{cID}/payroll/schedules/{id}.

Parent → children convenience wrappers:

ts
schedule.rates(opts: QueryRequest) // StaffRate.query filtered by payrollScheduleID — read-only from here; a rate can only be
                                    // created from a Staff's own Rates tab, where staffID is known
schedule.runs(opts: QueryRequest)  // PayrollRun.query filtered by payrollScheduleID

PayrollRun

company/payroll/run.ts. One processing run over a PayrollSchedule's pay period — batches its StaffRates' worked hours/bonuses into PayrollRunLines and a payout total.

FieldTypeNotes
id, companyID, payrollScheduleIDstring
periodStart / periodEndDate
statusPayrollRunStatus = "draft" | "processing" | "processed" | "paid"
totalAmountnumbercomputed by Process(), not hand-entered
processedAtDate | null?full data only — only ever set by Process()
paidAtDate | null?full data only — only ever set by MarkPaid()

PayrollRunStatusOptions — display labels in lifecycle order: "Draft", "Processing", "Processed", "Paid".

ts
static create(company: Company, runs: CreatePayrollRunData[]): Promise<EventData<string[]>>
async save(): Promise<SimpleResult<EventData>>
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollRun[]>>

CreatePayrollRunData = { payrollScheduleID: string; periodStart: Date; periodEnd: Date } — a run is created as draft with just a schedule + period; totals/lines come later from Process().

Endpoint: /company/{cID}/payroll/runs. viewHref: /company/{cID}/payroll/runs/{id}.

ts
run.schedule // PayrollSchedule | undefined, from payrollScheduleID — cached in a private field
ts
run.lines(opts: QueryRequest) // PayrollRunLine.query filtered by payrollRunID — rows are system-generated by Process(), not hand-created

Status helpers over the raw status string:

ts
run.IsDraft: boolean
run.IsProcessing: boolean
run.IsProcessed: boolean
run.IsPaid: boolean

Actions:

ts
async Process(): Promise<SimpleResult<EventData>>  // POST /company/{cID}/payroll/runs/process, body { payrollRunID: this.id }
async MarkPaid(): Promise<SimpleResult<EventData>> // POST /company/{cID}/payroll/runs/pay,     body { payrollRunID: this.id }
Note:Process() asks the backend to compute regular/overtime/bonus totals for every staff rate on the schedule and generate the PayrollRunLine rows — the frontend never computes these itself, it just triggers the calculation and re-reads the result. MarkPaid() marks a processed run as paid.

PayrollRunLine

company/payroll/runLine.ts. One staff member's computed line within a PayrollRun — worked units and their dollar amounts, split into regular/overtime/bonus. Normally system-generated by run.Process() rather than hand-created — mirrors Discount's "read-only-ish record with full CRUD methods" pattern.

FieldTypeNotes
id, companyID, payrollRunID, staffID, rateIDstring
regularUnitsnumber
overtimeUnitsnumber
regularAmountnumber
overtimeAmountnumber
bonusAmountnumber
totalAmountnumber
ts
static create(company: Company, lines: CreatePayrollRunLineData[]): Promise<EventData<string[]>> // rarely called directly — see PayrollRun.Process()
async save(): Promise<SimpleResult<EventData>>  // rarely called directly — lines are normally regenerated wholesale by Process()
async delete(): Promise<SimpleResult<EventData>>
static query(company: Company, data: QueryRequest): Promise<SimpleResult<PayrollRunLine[]>>

Endpoint: /company/{cID}/payroll/runs/lines. viewHref: /company/{cID}/payroll/lines/{id}.

Related-entity getters, each cached in a private field:

ts
line.run    // PayrollRun | undefined, from payrollRunID
line.staff  // Staff | undefined, from staffID
line.rate   // StaffRate | undefined, from rateID

How it fits together

Staff ──has one──> StaffRate ──attached to──> PayrollSchedule
                       │                            │
                       ├─ bonuses ──> StaffPayrollBonus
                       │                            │
StaffTimeBlock ─(via staffID)──────────────────────┘
   (worked hours, breaks tracked via TimeBlockBreak)
                       │
                       ▼
        PayrollRun.Process() batches worked
        StaffTimeBlocks + StaffRate + bonuses
        for every rate on the schedule
                       │
                       ▼
              PayrollRunLine (per staff member)
                       │
                       ▼
        PayrollRun.MarkPaid() → status "paid"

A Staff member gets one StaffRate (enforced 1:1 per (staffID, companyID)), which attaches to a PayrollSchedule and may carry a StaffPayrollBonus (also 1:1 per (rateID, companyID)). The staff member clocks in/out via StaffTimeBlock/TimeBlockBreak rows. When a PayrollRun is opened for the schedule's period and Process()d, the backend reads all of the above and writes one PayrollRunLineper staff member with the computed regular/overtime/bonus amounts — the client only ever triggers this and re-fetches, it doesn't do the math. MarkPaid() then closes out the run.