FinBaze for Platforms

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. externalId is yours, so sending a shift again updates it.
  • endAt must be after startAt, and the break must be shorter than the shift.
  • hourType is 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: draft can be corrected; status: approved is 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 Z or an offset.
  • Putting personal identifiers such as a BSN in metadata. Metadata is plain strings and visible to the company's users.