Guides
A scheduling product knows who worked when. FinBaze payroll turns approved hours into pay. You push employees, their employments (the contract), and worked hours; FinBaze keeps the books and the payroll. Plnnrr, a shift planner, is the worked example: its shifts become worked-hours entries one for one.
1. Employees and employments#
mutation Employee($company: CompanyRef!) {
upsertEmployee(input: {
company: $company
externalId: "plnnrr-emp-31"
employee: { firstName: "Sam", lastName: "Jansen", email: "sam@example.com", employeeNumber: "31" }
}) { status id }
}
mutation Employment($company: CompanyRef!) {
upsertEmployment(input: {
company: $company
externalId: "plnnrr-emp-31-c1"
employment: {
employee: { externalId: "plnnrr-emp-31" }
startDate: "2026-01-01"
contractType: indefinite
callType: min_max
hoursPerWeek: 24
}
}) { status id }
}
The employment is what hours attach to; hours cannot move to another contract. Wage, tax numbers and the pay run setup stay with the company in FinBaze; your product does not send them.
2. A hours feed#
Register a data source of type hours_feed and attach it:
mutation {
registerDataSource(input: { key: "plnnrr-hours", type: hours_feed, name: "Plnnrr hours", cadence: hourly, autoAttach: true }) { id key }
}
3. Push the hours#
mutation Hours($company: CompanyRef!) {
ingestWorkedHours(source: "plnnrr-hours", company: $company, entries: [
{ externalId: "shift-9001", employment: { externalId: "plnnrr-emp-31-c1" }, workDate: "2026-03-02", startAt: "2026-03-02T08:00:00Z", endAt: "2026-03-02T16:30:00Z", breakMinutes: 30, status: "draft" }
]) { line status externalId error { code message path } }
}
- One entry per shift.
externalIdis yours, so sending a shift again updates it. endAtmust be afterstartAt, and the break must be shorter than the shift.hourTypeis an opaque id from the collective agreement's hour-type catalogue (night, weekend, overtime). Send it only if the company's agreement uses types.status: draftcan be corrected;status: approvedis final because payroll may already have used it. Correct hours before you approve them. An approved entry cannot be changed or deleted (OBJECT_FINALISED).- Results are per entry (
line,status,error), so one bad shift does not block the rest.
Send corrections for draft hours by calling ingestWorkedHours again with the same externalId.
4. After the hours#
The company runs and approves its pay runs in FinBaze. Wage tax filings follow the usual consent (Filing consent, tax nl_wage_tax), and payment of wages can be handed to your platform with a payments provider and kinds: ["wages"] (Delegated actions).
Try it in the sandbox#
seedTestCompany(input: { template: nl_bv_payroll }) gives a Dutch B.V. with the wage tax obligation. The template does not create employees or pay runs yet, so create them with the calls above, push a week of hours, and read the results.
Pitfalls#
- Approving hours too early.
- Sending a timestamp without a zone: use ISO 8601 with
Zor an offset. - Putting personal identifiers such as a BSN in
metadata. Metadata is plain strings and visible to the company's users.