Time off in Business Made is a module you switch on at HR › Time off › Setup (/hr/time-off/setup). After that, people ask for time off in their staff portal (/staff-portal/time-off), their approver decides in Approvals (/staff-portal/approvals) or at HR › Time off (/hr/time-off), and each decision moves their balance.
This page covers setting it up and handling requests. The screens, explained for people, are in the Business Made docs: Time off setup, Time off, Request time off, Approve requests, and the tutorial Time off end to end. Paid time off reaches a paycheck through Run payroll. Hours someone logs as PTO on a timesheet are in Timesheets and the time clock.
Business Made has no AI tools of its own for HR. Every route below is under business-made/leave/ and goes through call_appengine. When a body isn't shown here, use search_api and describe_endpoint.
Ask first
- Days or hours? Do they count time off in days or hours? Most small businesses use days.
- Which types do they give, and how much of each? For example: 10 days PTO, 5 days sick, unpaid as needed. Also ask which types need approval. The starter set (step 2) is a good default.
- Does it build up or come all at once? Accrued monthly, or the full amount on 1 January (or the hire date).
- Their working days and holidays, and whether any site differs.
- Who approves: each person's supervisor, then HR; the site manager, then HR; or HR alone. Who is HR?
- Starting balances: is anyone carrying days over from before?
- For a request: who, which type, which dates, a half day or not, and the reason.
Steps
1. HR › Time off › Setup: the basics, and switching it on
Read the current state first: GET business-made/leave/setup. It returns the settings, readiness, who counts as HR, a suggested approval flow, and the types and policies.
Real action: POST business-made/leave/setup/settings with what changes:
{
"unit": "days",
"leaveYear": { "type": "calendar" },
"workingDays": ["mon", "tue", "wed", "thu", "fri"],
"hrUsers": ["[email protected]"],
"approval": { "baseFlow": "supervisor-hr", "escalateAfterDays": 3, "dailyDigest": true }
}| Setting | Default | What it does |
|---|---|---|
unit | days | days or hours. Amounts in policies and balances are in this unit. |
leaveYear | { "type": "calendar" } | When the year resets. |
workingDays | Mon–Fri | Days a request counts. Weekends and other days off are skipped. A site can override this with ?businessLocationId=<id>. |
hrUsers | none | People named as HR. Without any, HR is the business's Owners and admins, and then the company email. |
approval.baseFlow | supervisor-hr | supervisor-hr: the person's supervisor, else HR. manager-hr: their site's manager, else HR. hr: HR always. |
approval.escalateAfterDays | 3 | A request still waiting after this many days moves to HR. |
approval.dailyDigest | on | A daily email to each approver who has requests waiting. |
Saving the settings re-matches every employee to a policy. Switch it on: POST business-made/leave/setup/enable { "enabled": true }.
Switching time off on also seeds the business's payroll earning types: all 12 standard ones, REG, OT, DT, HOL, SAL, PTO, HPAY, UNP, BON, COM, TIP and REI (tested). It also creates the leave-approval workflow.
readiness.state is ready once the basics are saved, there is at least one active type, there is an active default policy, someone resolves as HR, and an approval flow is chosen. readiness.missing names whatever is left. Balances are not part of readiness (tested). If nobody has a balance, requests are refused (step 4).
Check: GET business-made/leave/setup/readiness returns state: "ready". On screen, the Setup page shows each part ticked.
2. HR › Time off › Setup: types and the default policy
The quick way: POST business-made/leave/setup/starter { "types": true, "policy": true }. It never overwrites a type the business already has with the same code. It makes these types:
| Code | Type | Allowance | Needs approval | Starter amount |
|---|---|---|---|---|
PTO | PTO | accrued monthly | yes, 7 days' notice | 10 days |
SICK | Sick | accrued monthly | no | 5 days |
PERSONAL | Personal / Floating | fixed, yearly | yes, 2 days' notice | 2 days |
BEREAVE | Bereavement | per event | no | 3 days |
UNPAID | Unpaid | unlimited | yes | — |
JURY | Jury / civic duty | unlimited | no | — |
HOLIDAY | Public holiday | company holidays | no | — |
It also makes an active default policy that gives these amounts.
A type of your own: POST business-made/leave/types with the fields the Setup form sends:
| Field | What goes in |
|---|---|
title, code | The name, and a short code in capitals and _. |
category, type | For example paid and vacation. |
allowance.type | accrued, fixed, per-event or unlimited. Per-event and unlimited types are never checked against a balance. |
accrual.method | monthly, annual or none. |
requestSettings | requiresApproval, allowHalfDays, minAdvanceNoticeDays, maxConsecutiveDays, requiresDocumentation, documentationAfterDays. |
paySettings.isPaid | Whether it's paid time. |
isActive, color, sortOrder, description | Display and on/off. |
A type with requiresApproval: false is notify-only. A request for it is approved the moment it's sent (tested with Sick).
A policy: POST business-made/leave/policies with title, isDefault, status: "active", leaveTypes, entitlements ([{ "leaveTypeId": "<id>", "amount": 10, "tenureBands": [{ "minYears": 3, "amount": 15 }] }]), applicableTo (employmentTypes, departments, locations, jobLevels, minTenureYears), approvalRules (requireApproval, approvalLevels) and carryoverRules (allowCarryover, maxCarryoverHours). Then run POST business-made/leave/setup/recompute.
Each person gets the most specific active policy whose applicableTo matches them, and otherwise the default. With approvalLevels above 1, the first approval passes the request to HR for the final say.
Hands on: the type ids and the policy id.
Check: the Setup page lists the types and marks the default policy. POST setup/recompute returns how many people it stamped and how many matched no policy (unmatched).
3. HR › Time off › Setup: holidays and closures
- Suggested holidays:
GET business-made/leave/calendar/suggest?year=2026(add&location=<id>for a site). Accept the ones they want withPOST business-made/leave/calendar/accept{ "rows": [ … ] }. - One day of your own:
POST business-made/leave/calendar{ "date": "2026-12-24", "title": "Christmas Eve", "kind": "closure" }.kindisholiday,closureorspecial(a short day, withhours). It can also takeendDate,location,observed(default true) andpaid(default true). GET business-made/leave/calendarlists them.DELETE business-made/leave/calendar/<id>removes one.
Observed holidays and closures are skipped when a request is costed. Tested: a three-day request over a holiday cost two days.
4. HR › Time off › Balances: opening balances
Without a balance for the year, a request for a type that has a balance is refused with 400 NO_BALANCE_FOR_YEAR.
- Preview:
GET business-made/leave/setup/opening-balances?year=2026. Each person gets their policy's amount, scaled in three ways:- pro-rated from their hire date, for someone who started this year;
- by their hours, for part-time staff (hours per week ÷ 40);
- raised by any tenure band they've reached. For accrued types, it shows the share earned so far. Tested: a hire starting late October was given 2 of 10 days, and staff with long service got the 15-day tenure band.
- Commit:
POST business-made/leave/setup/opening-balances{ "year": 2026, "overrides": [{ "employeeId": "EMP-1042", "leaveTypeId": "<id>", "used": 3 }] }.overridesrecords days already taken. This writes balances for every current employee.
Stop: committing writes everyone's balances. Show the preview totals and wait for yes.
Later changes to one balance:
| Change | Real action |
|---|---|
| Add or take away days | POST business-made/leave/balances/<balanceId>/adjust { "adjustment": 2, "reason": "Covered the holiday weekend" }. It is added to what's available (tested). |
| Record an accrual | POST business-made/leave/balances/<balanceId>/accrue { "hours": 8, "accrualDate": "2026-11-01" } |
| See a person's balances | GET business-made/leave/balances/employee/<code> |
The monthly accruals run as a job. GET business-made/leave/accrual/status shows when they last ran.
Check: HR › Time off › Balances (/hr/time-off/balances) shows a row per person and type.
5. HR › People: who approves whom
With supervisor-hr, a request goes to the person's supervisor. Set one with POST business-made/leave/setup/supervisor { "employeeSk": "<sk>", "supervisor": "<manager's employee code>" }. GET business-made/leave/setup/people lists everyone with the approver each one resolves to, and why HR is used when it falls back (no supervisor, supervisor has no email, no manager at site).
Check: each person's approver in that list is the one the business expects.
6. HR › Time off: a request
The person asks in their staff portal: Time off › request. Through the API as that person: POST /staff-portal/time-off { "leaveTypeId": "<id>", "startDate": "2026-11-13", "endDate": "2026-11-13", "isPartialDay": false, "reason": "…" }. This creates the request and sends it in one go. If it's refused, the draft is removed. The portal refuses requests with 412 LEAVE_NOT_READY until readiness is ready. GET /staff-portal/time-off/cost?startDate=…&endDate=… shows what it would cost first.
On someone's behalf (HR):
POST business-made/leave/requests{ "employeeId": "EMP-1042", "leaveTypeId": "<id>", "startDate": "2026-11-13", "endDate": "2026-11-13", "isPartialDay": false, "reason": "…" }. This saves a draft.- Stop: sending it emails the approver and the person. Say so, and wait for yes.
POST business-made/leave/requests/<id>/submit{}.
What sending it does (tested):
- Cost. Only working days count. Weekends, closures and observed holidays are skipped. A single half day costs 0.5. In hours, a day is the person's hours per week ÷ working days.
- Checks:
| Refusal | Meaning |
|---|---|
400 NO_WORKING_DAYS | The dates hold no working day, for example a weekend. |
409 LEAVE_OVERLAP | The person already has a pending or approved request on one of those days. |
400 INSUFFICIENT_BALANCE | Not enough left. The response gives available and requested. |
400 NO_BALANCE_FOR_YEAR | No balance for that type in that year (step 4). |
- Warnings, which don't block the request: inside a blackout, short notice, or longer than the type's maximum run of days.
- A notify-only type is approved at once. The person and their approver get Your time off is recorded (
leave.auto_approved). - Otherwise the request goes
pending. The cost is held on the balance (tested: 3 available became 1 available and 2 pending). A task opens on theleave-approvalworkflow for the approver. The approver gets A time-off request needs your decision (leave.submitted) and the person gets Your time-off request is in (leave.received).
Hands on: the request id.
Check: GET business-made/leave/requests/<id>: status, approver, and escalatesAt (the date it moves to HR).
7. Approvals: deciding, editing and cancelling
Stop before each of these. Every one emails the person, and some email the approver too.
| Action | Real action | What it does |
|---|---|---|
| Approve | POST business-made/leave/requests/<id>/approve { "note": "Enjoy" } | The held days move to used (tested). The person gets Your time off is approved (leave.approved). With two approval levels, the first yes passes it to HR, and HR is emailed. |
| Decline | POST business-made/leave/requests/<id>/reject { "reason": "We're short that Friday" } | A reason is required (400 REASON_REQUIRED, tested). The held days go back. The person gets leave.rejected. |
| Change a waiting request | POST business-made/leave/requests/<id>/edit with new dates, type or reason | It keeps the same approver, re-checks the overlap and balance, and tells the approver. It is refused (409 ALREADY_PART_APPROVED) once the first of two approvers has said yes. |
| Send a declined one again | POST business-made/leave/requests/<id>/resubmit { "reason": "…" } | It works only for a declined request, or one withdrawn before a decision. It goes back to pending with the same approver, as a new round (tested). |
| Cancel | POST business-made/leave/requests/<id>/cancel { "reason": "Plans changed" } | A pending request: the held days go back and the approver is emailed (leave.cancelled, tested). An approved one: the used days are refunded (tested), and the person is emailed if someone else cancelled it. |
Approvers decide from Approvals in the staff portal (/staff-portal/approvals, POST /staff-portal/approvals/<id>/approve or /reject with { "note": "…" }). They can also decide on the approval task in the Approvals tray.
A second decision on a request already decided is refused with 400 BAD_STATUS (tested).
The approver's note is lost. The note sent with an approve or decline isn't kept. The request's approvalComments or rejectionReason, and the email to the person, carry the request's summary instead (for example "2 days · trip"). Tested on both: a decline with "we are short that day" was saved as "0.5 days · skilltest appointment". Until this is fixed, tell the person the reason another way if it matters.
Waiting too long. When a request is still pending after escalateAfterDays, it moves to HR. HR gets leave.escalated and the person gets leave.waiting. Later reminders follow. The daily digest lists each approver's waiting requests.
Check: GET business-made/leave/balances/employee/<code> shows the change. HR › Time off › Team (/hr/time-off/team) shows who's off.
8. HR › Time off › Reports
GET business-made/leave/reports/<kind>?year=2026, where kind is balances, usage, pending or liability. Add &businessLocationId=<id> for one site. "Who's off next week": GET business-made/leave/team?from=2026-11-09&to=2026-11-15. Without dates it covers the next 30 days. On screen: HR › Time off › Reports (/hr/time-off/reports).
When it's done
Tell the person:
- the unit, working days and approval flow, and who counts as HR;
- the types and how much each person gets, and which types need no approval;
- the holidays added, and that requests skip them;
- whose balances were opened, and any overrides;
- for a request: its cost, its status, who decides, and who was emailed.
Open HR › Time off (/hr/time-off).
What it can't do yet
- No AI tools for HR. Everything goes through
call_appengine. - The approver's note isn't kept (warning in step 7).
- Readiness doesn't check balances. Time off shows as ready while every request would be refused for want of a balance. Commit opening balances in the same session.
- The staff portal can't be used until readiness is
ready. HR's own route has no such check.
Don'ts
- Don't send a request, approve, decline or cancel without a yes. Each one emails people.
- Don't commit opening balances without showing the preview. It writes everyone's balances at once.
- Don't hand-edit
bm_leave_balancewithrepo_update. Use adjust or accrue, so the change is recorded. - Don't make a second default policy. Change the one there is, or give the new one an
applicableTo. - Don't set a person's supervisor to themselves, or by email. It is the manager's employee code.