docs
/
Courses — AppEngine

AppEngine Workforce APIs: Employees, Scheduling, HR and Staff Self-Service

Watch on YouTube ↗

Video chapters
  1. AppEngine Workforce APIs
  2. Prepare your organization and companion
  3. Find the two route families
  4. One person, different identities
  5. Create the employee
  6. Onboarding follows actual evidence
  7. Accept the received staff invitation
  8. Resolve an ambiguous employee identity
  9. Persist required pay and tax evidence
  10. Read, sign and confirm onboarding
  11. A draft is not the employee’s schedule
  12. Create a draft schedule and shift
  13. Publish and read the employee calendar
  14. Time belongs to its employee and period
  15. Clock in, reject duplicate, clock out
  16. Submit and approve training time
  17. Find leave setup and calendar cost
  18. The calendar decides the leave cost
  19. Calculate the PTO request cost
  20. Approve PTO and reread the balance
  21. A payslip is a caller-owned result
  22. Read the employee’s private payslip
  23. Request the employee’s PDF
  24. Inspect the returned pay document
  25. Verify cross-employee payslip refusal
  26. 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.

DatatypeWhat it holdsA few real fields
userA login — someone who can sign inemail, firstName, employeeId (the link to their employee record)
bm_applicantA job candidatepersonalInfo.firstName, personalInfo.email, stage, jobPostingId
bm_employeeThe person as an employeeemployeeId (the staff code, e.g. HC-0107), personalInfo.firstName, contactInfo.email, employment.hireDate, employment.jobTitle, employment.status, compensation.hourlyRate
bm_scheduleA rota, with its shifts inside ittitle, periodStart, periodEnd, status, shifts[] (each with employeeId, date, startTime, endTime, status)
bm_timesheetOne person's hours for a pay periodemployeeId, payPeriodStart, payPeriodEnd, timeEntries[], totalHours, status (draft → submitted → approved)
bm_leave_type, bm_leave_requestKinds of time off, and requests for itleaveTypeId, startDate, endDate, totalDays, status
bm_pay_stubA payslippayPeriodStart, payPeriodEnd, grossPay, netPay, taxes, deductions
bm_compensation_changeA pay change on its way through approvalemployeeSk, effectiveDate, previousCompensation, newCompensation, status
bm_offboardingSomeone leaving, with a checklistemployeeId, 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 gets 403 with code BACK_OFFICE_FORBIDDEN and 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 callAppEngine does for you
POST business-made/recruitment/board/applicantschecks 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/hiredmarks 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/employeescreates the employee, refusing a staff code already in use (409 EMPLOYEE_ID_EXISTS)
POST profile/user/employee/create-from-employeeinvites the employee to a staff login by email — or, if a login with that email already exists, links it
POST business-made/onboarding/:employeeIdstarts the new-hire checklist (profile, W-4, pay method, handbook, offer letter, I-9)
POST business-made/onboarding/:employeeId/completerefuses while any required item is open; otherwise closes onboarding and sets employment.status to active
POST business-made/schedules/:id/shiftsworks 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/publishpublishes every shift in the period, and emails each person whose shifts changed — once per person, not once per shift
POST staff-portal/timesheets/clock-infinds 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/submitchecks the sheet is the caller's own, is a draft and has entries, then marks it submitted
POST business-made/timesheets/:id/approveapproves a submitted sheet, refusing if the same days are already approved on another sheet
GET staff-portal/time-off/costcounts working days in a date range, skipping weekends, holidays and closures
POST staff-portal/time-offcosts the dates, checks the balance, picks the approver, holds the days as pending and notifies both sides
POST staff-portal/approvals/:id/approvelets the assigned approver decide, refusing anyone the request is not waiting on
GET staff-portal/payslips/:stubId/pdfchecks the payslip is the caller's own and returns the PDF
POST business-made/compensation/changes/:id/processapplies an approved pay change to the employee record and the payroll profile
POST business-made/offboarding/cases/:id/actionstarts, 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

  1. 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.
  2. 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 a cafe_staff collection.
  3. 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_locker collection 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:

VariableWhoUsed for
HR_TOKENThe café's manager, an owner/admin or a role with the HR, Time and Payroll areas/business-made/* routes
MAYA_TOKENMaya, 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

Employee created through the companion and saved by AppEngine.
Employee created through the companion and saved by AppEngine.

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

Actual received invitation accepted; the staff login was created. Tokens and the chosen password are not displayed.
Actual received invitation accepted; the staff login was created. Tokens and the chosen password are not displayed.

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

Current journey readback confirms the required profile, tax, payment and handbook evidence is complete. Optional offer-letter and I-9 entries remain separately labelled.
Current journey readback confirms the required profile, tax, payment and handbook evidence is complete. Optional offer-letter and I-9 entries remain separately labelled.

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

The manager published the saved schedule; the employee refreshed the same range and received the shift.
The manager published the saved schedule; the employee refreshed the same range and received the shift.

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:

actionWhat it does
accept, declineanswers a shift the manager offered her (decline takes a reason)
coverasks for someone to cover her shift; withdraw takes the request back
swapasks a named colleague (targetEmployeeId) to swap; GET staff-portal/schedule/colleagues?date=&startTime=&endTime= lists who is free
takeclaims 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

Separately entered training hours have been submitted and approved. The brief live clock session remains its own labelled entry.
Separately entered training hours have been submitted and approved. The brief live clock session remains its own labelled entry.

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, call GET staff-portal/timesheets/clock-in/active to 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 capture object; 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 423 with 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

Recorded PTO request for November9–10: two working days approved and the employee balance refreshed to eight.
Recorded PTO request for November9–10: two working days approved and the employee balance refreshed to eight.

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

The signed-in employee lists and opens her actual generated pay stub. This controlled check-method run did not send money.
The signed-in employee lists and opens her actual generated pay stub. This controlled check-method run did not send money.

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.pdf

The 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:

CallEffect
POST business-made/compensation/changes/:id/submitdraft → 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 seeWhat it meansWhat to do
403 No staff record for this accountthe login isn't linked to an employee and no employee has that emaillink the login (section 4)
403 BACK_OFFICE_FORBIDDENa staff member called a /business-made/* routeuse the matching /staff-portal/* route, or give their role the area
403 Not your timesheet / Not your payslipthe ID belongs to someone elseshow it as not found
400 ALREADY_CLOCKED_INshe's already on the clockread the active clock-in and show it
423 on clock-ina readiness rule blocks hershow the reasons; a manager can override
412 LEAVE_NOT_READYtime off isn't set up for the businessshow "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:

AreaRoutes start withCovers
Benefitsbusiness-made/benefitsplans, enrolments, dependants, beneficiaries
Documents and policiesbusiness-made/documentsemployee documents, signatures, policy versions and acknowledgements
Performancebusiness-made/performancegoals, reviews, improvement plans; staff acknowledge via staff-portal/reviews
Learningbusiness-made/learning, staff-portal/trainingcourses, certifications, required training
Leave setupbusiness-made/leaveleave types, policies, balances, accruals

API Reference shows how to read an operation's full contract.

Where to go next

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.