Video chapters
- AppEngine Workforce APIs
- Prepare your organization and companion
- Find the two route families
- One person, different identities
- Create the employee
- Onboarding follows actual evidence
- Accept the received staff invitation
- Resolve an ambiguous employee identity
- Persist required pay and tax evidence
- Read, sign and confirm onboarding
- A draft is not the employee’s schedule
- Create a draft schedule and shift
- Publish and read the employee calendar
- Time belongs to its employee and period
- Clock in, reject duplicate, clock out
- Submit and approve training time
- Find leave setup and calendar cost
- The calendar decides the leave cost
- Calculate the PTO request cost
- Approve PTO and reread the balance
- A payslip is a caller-owned result
- Read the employee’s private payslip
- Request the employee’s PDF
- Inspect the returned pay document
- Verify cross-employee payslip refusal
- A connected workforce client
Written guide
Before you start
Open Appmint Studio and use your organization's assigned AppEngine API origin and organization ID. Follow API Foundations: sign-in and request context (staff versus customer identity at 7:11) for obtaining a staff session and completing any configured authentication challenge. This course starts with an authorized manager in that organization; it does not need a root organization. The employee accepts a supported invitation and signs in separately.
Use the maintained AppEngine API reference. Search Staff Portal for caller-owned routes and BusinessMade for manager operations. Read the current operation's permission, body and errors before binding it to your client. The organization header selects a tenant; it does not grant a permission.
Run the companion application
Open the companion README for the runnable browser, JavaScript/TypeScript, Python, curl and Postman examples. Set APPENGINE_API_ORIGIN to your assigned backend, run node browser/server.mjs, and open its printed development address. Sign the manager and employee into their separate session slots. The local browser is a client implementation example; it calls AppEngine for every demonstrated result.
Prepare a controlled organization with the relevant HR, time, leave and payroll permissions, a current published handbook and a configured invitation delivery channel. Employee creation can trigger the configured default journey and invitation delivery. The leave chapter requires working days, an active policy and types, an approval route and the employee's actual opening balance. The payroll section links the separate employer and payroll preparation procedure before reading any payslip.
Why this course
You are building a staff app for a small business: new hires get a login, see their shifts, clock in from their phone, ask for a day off and read their payslips, while the manager hires, schedules and approves from the office. Writing that yourself means an employee database, a rota engine, a time clock that knows which pay period a punch belongs to, leave balances, approval routing and payslip PDFs. AppEngine already has all of it. Your app calls it; you build the screens.
Throughout the course we follow one person. Harbour Café hires Maya Lopez as a part-time barista. We take her from applicant to employee, give her a staff login, onboard her, put her on next week's schedule, let her clock a shift, request time off and read her payslip, then give her a raise and, eventually, see her out. Each step is one AppEngine call that does the business work for you.
1. What AppEngine already has for staff
Records you don't have to design
AppEngine stores everything as records. A record has a datatype (the kind of thing it is, such as bm_employee), an sk (its unique ID, generated by AppEngine) and a data object holding the fields. The workforce datatypes below are built in: their fields are already defined in the SDK (sdk/src/models/business-made), and BusinessMade's HR, Time and Payroll screens read and write the same records your app does.
| Datatype | What it holds | A few real fields |
|---|---|---|
user | A login — someone who can sign in | email, firstName, employeeId (the link to their employee record) |
bm_applicant | A job candidate | personalInfo.firstName, personalInfo.email, stage, jobPostingId |
bm_employee | The person as an employee | employeeId (the staff code, e.g. HC-0107), personalInfo.firstName, contactInfo.email, employment.hireDate, employment.jobTitle, employment.status, compensation.hourlyRate |
bm_schedule | A rota, with its shifts inside it | title, periodStart, periodEnd, status, shifts[] (each with employeeId, date, startTime, endTime, status) |
bm_timesheet | One person's hours for a pay period | employeeId, payPeriodStart, payPeriodEnd, timeEntries[], totalHours, status (draft → submitted → approved) |
bm_leave_type, bm_leave_request | Kinds of time off, and requests for it | leaveTypeId, startDate, endDate, totalDays, status |
bm_pay_stub | A payslip | payPeriodStart, payPeriodEnd, grossPay, netPay, taxes, deductions |
bm_compensation_change | A pay change on its way through approval | employeeSk, effectiveDate, previousCompensation, newCompensation, status |
bm_offboarding | Someone leaving, with a checklist | employeeId, terminationType, lastWorkingDate, checklist[], status |
Notice that an employee has two identities. sk is the record's ID; employeeId is the staff code people see on badges and payslips. Shifts, timesheets and payslips are keyed by the staff code. We'll use HC-0107 for Maya.
A user and a bm_employee are separate records on purpose: not every login is staff (a customer, an accountant), and not every employee has a login yet. The user.employeeId field joins them.
Two doors into the same data
AppEngine gives workforce apps two sets of routes:
/staff-portal/*— "me" routes. The server works out which employee the signed-in person is and only ever acts on that employee. There is no employee ID in the request, so Maya's app cannot ask for someone else's payslip./business-made/*— back-office routes. These take an explicit employee and are for managers. Each controller belongs to a BusinessMade area (/hr,/time,/payroll,/operations/scheduling). Owners and admins can use every area; anyone else needs a role that has been given that area. A staff member without it gets403with codeBACK_OFFICE_FORBIDDENand the message pointing them to the staff portal.
Actions that do the work for you
These are the built-in actions this course uses. Each one runs the business rules on the server.
| You call | AppEngine does for you |
|---|---|
POST business-made/recruitment/board/applicants | checks first name, last name and email are present, puts the candidate at stage new, records when they applied |
POST business-made/recruitment/board/applicants/:id/hired | marks the applicant hired, links them to the new employee record, counts the hire against the job and closes the job when every opening is filled |
POST business-made/employees | creates the employee, refusing a staff code already in use (409 EMPLOYEE_ID_EXISTS) |
POST profile/user/employee/create-from-employee | invites the employee to a staff login by email — or, if a login with that email already exists, links it |
POST business-made/onboarding/:employeeId | starts the new-hire checklist (profile, W-4, pay method, handbook, offer letter, I-9) |
POST business-made/onboarding/:employeeId/complete | refuses while any required item is open; otherwise closes onboarding and sets employment.status to active |
POST business-made/schedules/:id/shifts | works out paid hours (overnight and break aware), refuses a shift that overlaps one the person already has (409 SHIFT_CONFLICT) and checks the person is allowed to work it |
POST business-made/schedules/week/publish | publishes every shift in the period, and emails each person whose shifts changed — once per person, not once per shift |
POST staff-portal/timesheets/clock-in | finds the employee, refuses a second clock-in, checks they are cleared to work, opens the right timesheet for the pay period and records the clock event |
POST staff-portal/timesheets/:id/submit | checks the sheet is the caller's own, is a draft and has entries, then marks it submitted |
POST business-made/timesheets/:id/approve | approves a submitted sheet, refusing if the same days are already approved on another sheet |
GET staff-portal/time-off/cost | counts working days in a date range, skipping weekends, holidays and closures |
POST staff-portal/time-off | costs the dates, checks the balance, picks the approver, holds the days as pending and notifies both sides |
POST staff-portal/approvals/:id/approve | lets the assigned approver decide, refusing anyone the request is not waiting on |
GET staff-portal/payslips/:stubId/pdf | checks the payslip is the caller's own and returns the PDF |
POST business-made/compensation/changes/:id/process | applies an approved pay change to the employee record and the payroll profile |
POST business-made/offboarding/cases/:id/action | starts, works through and completes the leaving checklist, refusing completion while required tasks are open |
Here is one of them in full, so you can see what "AppEngine does it for you" looks like on the wire. Maya taps Clock in in her app:
curl -X POST "$API/staff-portal/timesheets/clock-in" \
-H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes":"Opening shift"}'{
"timesheet": {
"sk": "6720f1c2a9e4b70012ab34cd",
"datatype": "bm_timesheet",
"data": {
"employeeId": "HC-0107",
"payPeriodStart": "2026-10-12",
"payPeriodEnd": "2026-10-25",
"status": "draft",
"timeEntries": [ { "id": "te-1760511600000-k3x9qa", "date": "2026-10-15", "startTime": "2026-10-15T06:58:12.000Z", "clockedIn": true, "description": "Opening shift" } ]
}
},
"entry": { "id": "te-1760511600000-k3x9qa", "date": "2026-10-15", "startTime": "2026-10-15T06:58:12.000Z", "type": "regular", "clockedIn": true, "shiftId": "shift-1760300000000-p2m8rt" },
"clockEvent": { "direction": "in", "timesheetId": "6720f1c2a9e4b70012ab34cd", "entryId": "te-1760511600000-k3x9qa" },
"readiness": null,
"trainingAtClockIn": []
}The request said nothing about who Maya is, which timesheet to use or which shift she is on. The server resolved her from her login, matched the punch to today's shift, created or reused the timesheet for her pay period and logged a clock event for the manager's view.
The rule
- Use the built-in action when one exists — hiring, clocking in, requesting leave, publishing a rota, approving. Writing the record yourself (for example, pushing an entry into a timesheet with a generic update) skips everything in the right-hand column above.
- Use the built-in datatype for the thing itself and add your own fields to it. If the café wants to store each barista's shirt size, add a field to
bm_employee— your organization's fields are merged over the built-in collection — rather than creating acafe_staffcollection. - Create your own collection only for something AppEngine doesn't have. Harbour Café assigns lockers, and there is no locker datatype, so define a
staff_lockercollection in Studio Manager → Database and use the generic Repository routes:
curl -X PUT "$API/repository/create" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"datatype":"staff_locker","data":{"lockerNumber":"12","employeeId":"HC-0107"}}'
curl -X POST "$API/repository/find/staff_locker" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"query":{"data.employeeId":"HC-0107"},"options":{"page":1,"pageSize":20}}'Repository and Database covers collections in depth.
2. Set up the two people who will call the API
Every request carries two headers:
orgid— your organization ID, the identifier of the business account (Harbour Café). AppEngine keeps each organization's records apart.Authorization: Bearer <token>— a bearer token, the session token returned when a person signs in. It tells AppEngine who is calling.
You need two sessions for this course:
| Variable | Who | Used for |
|---|---|---|
HR_TOKEN | The café's manager, an owner/admin or a role with the HR, Time and Payroll areas | /business-made/* routes |
MAYA_TOKEN | Maya, once she has accepted her invitation in section 4 | /staff-portal/* routes |
Create your organization and first client shows how to get API, ORG and a staff session; Authentication and organization isolation covers sessions in more depth. If you are building in Flutter or another connected client, keep the two sessions in separate token stores (Connected clients).
3. Hire Maya

Maya applied at the counter. The manager adds her to the hiring board:
curl -X POST "$API/business-made/recruitment/board/applicants" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"personalInfo":{"firstName":"Maya","lastName":"Lopez","email":"[email protected]","phone":"555-0142"},"source":"other"}'The response is the applicant as the hiring screen shows it: id (the applicant's sk), stage: "new", the next suggested stage and a stages list. Add "jobPostingId" to file her under an open job from GET business-made/recruitment/board; an unknown job is refused.
Move her along as the interviews happen with the applicant action route. action is one of stage, reject, withdraw, reopen or notes:
curl -X POST "$API/business-made/recruitment/board/applicants/$APPLICANT_ID/action" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"action":"stage","stage":"interview"}'The stages are new, screening, interview, assessment, offer. Hiring is its own step, because it creates a real employee. First create the employee record:
curl -X POST "$API/business-made/employees" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{
"employeeId": "HC-0107",
"personalInfo": {"firstName": "Maya", "lastName": "Lopez"},
"contactInfo": {"email": "[email protected]", "phone": "555-0142"},
"employment": {"hireDate": "2026-10-12", "jobTitle": "Barista", "employmentType": "part-time"},
"compensation": {"payType": "hourly", "hourlyRate": 17}
}'The body is the employee's data fields, sent flat. The response is the saved record — keep its sk as EMPLOYEE_SK. Then tell the hiring board which employee she became:
curl -X POST "$API/business-made/recruitment/board/applicants/$APPLICANT_ID/hired" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d "{\"employeeId\":\"$EMPLOYEE_SK\"}"This route's employeeId is the employee record's sk, not the staff code. AppEngine sets her stage to hired, stores the link and, if she was hired for a job, counts the opening as filled.
4. Give Maya her staff login

Creating Maya can already start an onboarding journey whose provisioning task creates her login and sends an invitation. Read the employee's current journey before assuming no login exists. If your integration needs to create or link the account explicitly, call:
curl -X POST "$API/profile/user/employee/create-from-employee" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"employeeId":"HC-0107","redirectUrl":"https://studio.appmint.io/invite/complete"}'{ "success": true, "invitedNewUser": true, "invitation": { "...": "the invitation record" }, "employeeId": "HC-0107" }AppEngine takes the email from contactInfo.email and sends Maya an invitation to a staff account. If a login with that email already exists, it links that login instead and returns linkedExistingUser: true.
Maya opens the invitation link delivered to her email. Its org and token identify the invitation; the client validates them with POST profile/user/invite/validate, then sends orgId, invitationToken, her chosen password and name to POST profile/user/invite/complete. Keep those tokens out of visible logs. An existing unclaimed login created by onboarding is claimed through this flow. linkedExistingUser is not proof that the recipient has accepted. After she signs in through the supported authentication flow, her app holds MAYA_TOKEN. Every staff-portal call starts by finding her employee record: first through user.employeeId, then by matching her email to contactInfo.email. Check it worked:
curl "$API/staff-portal/profile" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"The response is her employee fields with the sk on top and the compensation section removed — pay belongs to payroll screens, not the profile form. If email matching remains ambiguous (including no unique full-name match), the portal answers 403 "More than one staff record uses this email". Link the login explicitly and the ambiguity goes away:
curl -X POST "$API/profile/user/employee/link" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"userId":"MAYA_USER_SK","employeeId":"HC-0107"}'Maya can edit her own name, phone, address and emergency contact with PUT staff-portal/profile — for example {"preferredName":"May","emergencyContact":{"name":"Ana Lopez","relationship":"sister","phone":"555-0199"}}. Job title, roles and pay are not in that list; HR changes those.
For the colleague picker in her app (who's on today, who to message), use GET staff-directory?search=…&department=…. It returns work contact fields — name, email, title, department, location, company phone, roles — without any private HR data.
5. Onboard her

The current onboarding routes mirror the journey engine. Employee creation may already have started a journey, so first read GET business-made/onboarding/HC-0107. Use its journeyId to inspect GET business-made/journeys/:id, or the employee's own onboarding/tasks views. Render the returned stages, required flags, evidence and available actions. Do not assume every organization has the same six manual checklist items.
In the default journey, creating the login and payroll profile are provisioning tasks. Complete profile data can close its evidence task automatically. The remaining required tasks in our controlled example were pay choice, W-4 evidence and the current handbook acknowledgement. There are also optional first-week and later check-ins; closing required onboarding does not claim those later activities were demonstrated.
As the employee, choose the actual payment method through PUT staff-portal/pay-method, for example {"payMethod":"check"}. An ACH choice also needs a usable bank-account setup; choosing the label is not enough. Employee tax elections are read and saved through GET/PUT staff-portal/tax-info, using the current federalTax and stateTax shape. The W-4 task requires recorded form-year/signed evidence, not merely the default filing status. The course fixture uses synthetic values and makes no filing.
Read GET staff-portal/training/policies, then the selected policy's detail. Render its current text and resources. Report opening the policy or a required resource with POST staff-portal/training/policies/:policyId/viewed. After the person reads it, collect their typed name or drawn signature and explicit electronic-signature consent, then acknowledge:
{
"signature": {
"typedName": "Maya Lopez",
"consent": true
}
}Send that body to POST staff-portal/training/policies/:policyId/acknowledge. Reopen the onboarding state to observe the real evidence. A task's generic completion route is not a substitute for reading or signing the document. Optional tasks may have a supported skip route and recorded reason; do not shorten the required journey by assuming omitted items are skipped.
Refresh the journey first: evidence updates may already have completed it. If it remains in progress, call POST business-made/onboarding/HC-0107/complete. Do not repeat completion against an already-complete journey. In the actual walkthrough it returned412 while required evidence was missing, then complete after the evidence was saved. Preserve that error and its outstanding items in the client so the person can finish the right task.
6. Put her on next week's schedule

A schedule (bm_schedule) holds shifts. Find the café's schedule with GET business-made/schedules, or create one:
curl -X POST "$API/business-made/schedules" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Front of house","periodStart":"2026-10-12","periodEnd":"2026-10-25"}'Add Maya's shift. employeeId here is her staff code:
curl -X POST "$API/business-made/schedules/$SCHEDULE_ID/shifts" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"employeeId":"HC-0107","employeeName":"Maya Lopez","date":"2026-10-15","startTime":"07:00","endTime":"13:00","breakMinutes":30,"position":"Barista"}'The response is the new shift with its id, the paid duration (5.5 hours here) and status: "draft". A draft is the manager's working copy: Maya can't see it yet. When the week is ready, publish it:
curl -X POST "$API/business-made/schedules/week/publish" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"weekStart":"2026-10-12","lengthDays":7}'{ "weekStart": "2026-10-12", "lengthDays": 7, "periodEnd": "2026-10-18", "shifts": 24, "published": 3, "unchanged": 21, "people": 2, "notified": 2, "readiness": null }Only shifts that changed since the last publish count as published, and only the people on them are emailed. Add businessLocationId when the business has several locations. If someone on the rota isn't cleared to work a shift (missing training, for example), readiness explains why.
Maya now sees it in her app:
curl "$API/staff-portal/schedule/upcoming?days=14" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"Her own shift actions all go through one route, POST staff-portal/schedule/:scheduleId/shifts/:shiftId/:action:
action | What it does |
|---|---|
accept, decline | answers a shift the manager offered her (decline takes a reason) |
cover | asks for someone to cover her shift; withdraw takes the request back |
swap | asks a named colleague (targetEmployeeId) to swap; GET staff-portal/schedule/colleagues?date=&startTime=&endTime= lists who is free |
take | claims an open shift from GET staff-portal/schedule/takeable |
Show shift times in the business's time zone, which is how they are stored, rather than converting them to the phone's zone.
7. She works the shift

On the day, Maya clocks in — the call you saw in section 1 — and out at the end:
curl -X POST "$API/staff-portal/timesheets/clock-out" \
-H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN" -H "Content-Type: application/json" \
-d '{"notes":"Closed the espresso bar"}'AppEngine closes the open entry and works out the hours. Three things her app should handle:
- A second tap. Clocking in twice answers
400 ALREADY_CLOCKED_IN. If a request times out, callGET staff-portal/timesheets/clock-in/activeto see whether she is clocked in before trying again. - Location rules. If the café requires a photo or an on-site location to clock in, send what the phone captured in a
captureobject; Clock and timesheet and Multi-location show where those rules are set. - Not cleared to work. If a readiness rule blocks her (an expired food-hygiene certificate, say), clock-in answers
423with the reasons. Show them; a manager can approve an override.
At the end of the pay period Maya reviews her sheet and sends it in:
curl "$API/staff-portal/timesheets/current" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"
curl -X POST "$API/staff-portal/timesheets/$TIMESHEET_ID/submit" \
-H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"The sheet moves from draft to submitted. Show "Submitted" and "Approved" as different states in her app — submitted means waiting for the manager. The manager approves it (or rejects it with a reason):
curl -X POST "$API/business-made/timesheets/$TIMESHEET_ID/approve" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN"Approved timesheets are what payroll pays from.
8. She asks for time off

Start by reading what Maya can request:
curl "$API/staff-portal/time-off" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"The response has state, the types she may request, her balances for the year, her existing requests and her approvers. If state is off or not yet ready, time off hasn't been switched on for the café; HR sets it up in BusinessMade (HR: configure and approve time off). Show that as "not available yet" rather than an empty form.
Before she submits, show her what the dates will cost:
curl "$API/staff-portal/time-off/cost?startDate=2026-11-02&endDate=2026-11-06" \
-H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"{ "unit": "days", "amount": 3, "calendarDays": 5, "workingDays": 3, "skipped": [ { "date": "2026-11-03", "reason": "not a working day" }, { "date": "2026-11-05", "reason": "not a working day" } ], "totalDays": 3, "totalHours": 18, "hoursPerDay": 6 }As a part-timer, two of those days aren't hers to work, so the request costs three days. Then submit:
curl -X POST "$API/staff-portal/time-off" \
-H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN" -H "Content-Type: application/json" \
-d '{"leaveTypeId":"LEAVE_TYPE_SK","startDate":"2026-11-02","endDate":"2026-11-06","reason":"Family visit"}'The request comes back pending, with the approver it was sent to in its approvalChain (types that need no approval are approved straight away). If the dates hold no working days or her balance is too low, the request is refused with the reason (NO_WORKING_DAYS, a balance message) and nothing is saved. Send "draft": true to save without submitting.
Her approver — her supervisor or HR, as the café has set it up — sees the requests waiting on them and decides:
curl "$API/staff-portal/approvals" -H "orgid: $ORG" -H "Authorization: Bearer $APPROVER_TOKEN"
curl -X POST "$API/staff-portal/approvals/$REQUEST_ID/approve" \
-H "orgid: $ORG" -H "Authorization: Bearer $APPROVER_TOKEN" -H "Content-Type: application/json" \
-d '{"note":"Enjoy the trip"}'Use reject in place of approve to decline; a note is then required, because Maya needs to know why. Maya can change a pending request with POST staff-portal/time-off/:id/edit, send a declined one again with …/resubmit, or withdraw it with …/cancel.
9. She reads her pay

Payroll runs are covered in Accounting and Payroll APIs. Once a run has produced Maya's payslips, her app lists and opens them:
curl "$API/staff-portal/payslips?page=1&pageSize=12" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"
curl "$API/staff-portal/payslips/$STUB_ID" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN"
curl "$API/staff-portal/payslips/$STUB_ID/pdf" -H "orgid: $ORG" -H "Authorization: Bearer $MAYA_TOKEN" -o payslip.pdfThe list is a page of bm_pay_stub records (data array, total). Each detail and PDF call checks the stub belongs to the caller; anyone else gets 403 Not your payslip. The PDF route returns application/pdf bytes, so download it with a binary-capable HTTP call rather than a JSON helper.
GET staff-portal/payslips/year/2026/summary gives her year to date (count, gross, taxes, deductions, net), and GET staff-portal/w2/2026/pdf her W-2. She chooses how she is paid with PUT staff-portal/pay-method (ach, check or cash) and adds a bank account with POST staff-portal/direct-deposit; account numbers come back masked to the last four digits.
10. Handle the next case
Maya gets a raise
Pay changes go through approval and keep a history, so don't edit compensation on the employee directly. Create the change:
curl -X POST "$API/business-made/compensation/changes" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d "{\"employeeSk\":\"$EMPLOYEE_SK\",\"changeType\":\"merit-increase\",\"effectiveDate\":\"2026-12-01\",\"newCompensation\":{\"payType\":\"hourly\",\"hourlyRate\":18.5,\"payFrequency\":\"bi-weekly\"},\"reason\":\"Three months, great reviews\"}"AppEngine reads her current pay into previousCompensation, works out changeAmount and changePercentage, and saves the change as draft. Then it moves through three calls, each refusing the wrong starting state:
| Call | Effect |
|---|---|
POST business-made/compensation/changes/:id/submit | draft → pending-approval |
POST business-made/compensation/changes/:id/approve with {"comments":"…"} | → approved (or /reject with a reason) |
POST business-made/compensation/changes/:id/process | → processed, writing the new rate to her employee record and payroll profile from the effective date |
GET business-made/compensation/view/pay/:employeeSk shows her pay as HR sees it. Until it is processed, a change can be withdrawn with POST …/changes/:id/cancel; once it has gone for approval it is part of her pay history and is cancelled, not deleted.
Maya moves away
When Maya hands in her notice, open an offboarding case:
curl -X POST "$API/business-made/offboarding" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"employeeId":"HC-0107","employeeName":"Maya Lopez","terminationType":"voluntary-resignation","terminationReason":"relocation","lastWorkingDate":"2027-03-31"}'AppEngine adds the standard checklist — HR, IT, facilities, manager and finance tasks such as final pay, collecting keys and revoking system access. GET business-made/offboarding/cases/:id returns the case with its checklist and the moves allowed right now. Work it with one route:
curl -X POST "$API/business-made/offboarding/cases/$CASE_ID/action" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"action":"task-done","taskId":"task-11"}'action is start, task-done, task-na (not applicable, with a reason), complete or cancel. complete is refused while required tasks are open. The current platform also has journey-backed offboarding with a required login-disable provisioning task scheduled for its effective time. Inspect the actual journey/template and provisioning result; do not assume a checklist tick itself revoked access, or that every deployment still requires a separate manual Admin → Users change. Verify the departing account can no longer sign in at the intended time. This advanced path is described here but is not part of the recorded core workforce sequence.
On her last day, end her employment:
curl -X POST "$API/business-made/employees/$EMPLOYEE_SK/terminate" \
-H "orgid: $ORG" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/json" \
-d '{"reason":"Relocated","terminationType":"voluntary","effectiveDate":"2027-03-31","eligibleForRehire":true}'Her employment.status becomes terminated with the end date and reason recorded. Her timesheets and payslips stay, which is what payroll and year-end forms need.
When a call is refused
| What you see | What it means | What to do |
|---|---|---|
403 No staff record for this account | the login isn't linked to an employee and no employee has that email | link the login (section 4) |
403 BACK_OFFICE_FORBIDDEN | a staff member called a /business-made/* route | use the matching /staff-portal/* route, or give their role the area |
403 Not your timesheet / Not your payslip | the ID belongs to someone else | show it as not found |
400 ALREADY_CLOCKED_IN | she's already on the clock | read the active clock-in and show it |
423 on clock-in | a readiness rule blocks her | show the reasons; a manager can override |
412 LEAVE_NOT_READY | time off isn't set up for the business | show "not available yet" |
11. Find the rest in the API reference
You've now used the routes a staff app needs and know what to look for next. Every AppEngine server publishes its reference at /documentation. Search it by subject:
curl "$API/documentation/search?q=staff%20portal"
curl "$API/documentation/search?q=offboarding"Each result leads to the operation's parameters and body. The HR areas this course didn't walk through follow the same pattern — a screen-shaped view/… read and one action route per step:
| Area | Routes start with | Covers |
|---|---|---|
| Benefits | business-made/benefits | plans, enrolments, dependants, beneficiaries |
| Documents and policies | business-made/documents | employee documents, signatures, policy versions and acknowledgements |
| Performance | business-made/performance | goals, reviews, improvement plans; staff acknowledge via staff-portal/reviews |
| Learning | business-made/learning, staff-portal/training | courses, certifications, required training |
| Leave setup | business-made/leave | leave types, policies, balances, accruals |
API Reference shows how to read an operation's full contract.
Where to go next
- BusinessMade HR and Scheduling — set up the leave types, shift definitions and approvers your app relies on.
- Workforce Readiness — the rules behind the
423"not cleared to work" answers. - Accounting and Payroll APIs — run payroll from approved timesheets.
- Files and Storage — upload employee documents.
Verified course fixture and companion status — 17 October 2026
Live controlled proof covers employee creation and invitation acceptance; linked employee profile; required onboarding completion; draft-to-published schedule visibility; clock in/out and duplicate refusal; separately entered7.5training hours submitted/approved/read back; paid leave cost2days, approval and balance10→8; own payslip/detail/PDF/year summary; ambiguous email403 resolved by explicit employee-code link; another employee refused403 for the first employee's payslip. Internal payroll processing generated the fixture's stub; no bank transfer or filing was sent. The matching final video has now passed independent encoded audio and sampled graphics review; the player passed all 26 chapter seeks. Year-summary and shift-acceptance alternatives remain written/portable exercises, not filmed demonstrations.
Run the published browser and portable source companion. The browser is plain JavaScript. JavaScript/TypeScript, Python, curl and Postman examples accompany the same contracts. Companion source is published at commit 607ac0f9bcce8a5fe6a8edcfbebae253c9420d14. Watch the complete 7:20 course, ready for review: 26 chapters, Chris narration, 2560 × 1440 at 30 fps.
Payroll setup and generation prerequisites: Accounting and Payroll APIs, payroll section. Its video is not yet available; use the written section until it is published.