Leave API
Endpoints
Section titled “Endpoints”GET /api/leave/types
Section titled “GET /api/leave/types”List cached leave types. Provider-aware: fetches from Xero PayItems for Xero employees, or from Remote time-off types for Remote employees. Auto-refreshes if cache is older than 1 hour.
Permission: view:leave
Response:
{ "types": [ { "id": "xero-leave-type-id", "name": "Annual Leave", "is_paid": 1 } ], "provider": "xero"}For Remote employees, leave type IDs are prefixed with remote: (e.g. remote:timeoff-type-uuid).
GET /api/leave/balances
Section titled “GET /api/leave/balances”Get cached leave balances for the current user. Provider-aware: uses Xero employee balances or aggregates Remote approved time-off days by type.
Permission: view:leave
Response:
{ "balances": [ { "id": "uuid", "leave_type_id": "xero-id", "leave_type_name": "Annual Leave", "balance_hours": 152.0, "fetched_at": "2026-03-01T10:00:00" } ], "provider": "xero"}For Remote employees, balance_hours is calculated from approved time-off days (days × 8 hours).
POST /api/leave/balances/refresh
Section titled “POST /api/leave/balances/refresh”Force-refresh balances from the payroll provider (Xero or Remote), bypassing the 1hr cache.
Permission: update:leave
Response: Same as GET /api/leave/balances.
GET /api/leave/requests
Section titled “GET /api/leave/requests”List leave requests scoped by access level and tab.
Permission: view:leave
Query params:
tab—mine(own requests),pending(awaiting your approval),all(all requests, head/exec only)
Response:
{ "requests": [ { "id": "uuid", "user_id": "uuid", "user_name": "Name", "user_picture": "https://...", "leave_type_name": "Annual Leave", "start_date": "2026-03-10", "end_date": "2026-03-14", "total_hours": 40, "status": "pending_exec", "created_at": "2026-03-01T10:00:00" } ]}GET /api/leave/requests/:id
Section titled “GET /api/leave/requests/:id”Get a single leave request with full details and history.
Permission: view:leave
Response:
{ "request": { "id": "uuid", "user_id": "uuid", "user_name": "Name", "leave_type_id": "xero-id", "leave_type_name": "Annual Leave", "start_date": "2026-03-10", "end_date": "2026-03-14", "total_hours": 40, "notes": "Family holiday", "status": "approved", "manager_approved_by": "uuid", "manager_approved_at": "2026-03-02T09:00:00", "exec_approved_by": "uuid", "exec_approved_at": "2026-03-02T10:00:00", "xero_leave_application_id": "xero-id", "remote_timeoff_id": "remote-timeoff-id", "productive_booking_id": "123", "google_calendar_event_id": "gcal-id", "slack_message_ts": "1234567890.123456", "integration_errors": null, "created_at": "2026-03-01T10:00:00" }, "history": [ { "id": "uuid", "action": "submitted", "actor_id": "uuid", "actor_name": "Name", "comment": null, "created_at": "2026-03-01T10:00:00" } ]}POST /api/leave/requests
Section titled “POST /api/leave/requests”Submit a new leave request. Initial status depends on the submitter’s access level:
- Executive →
approved(auto-approved, integrations run immediately) - Manager/Head →
pending_exec - Lead/Employee →
pending_manager
Permission: update:leave
Body:
{ "leave_type_id": "xero-leave-type-id (required)", "leave_type_name": "Annual Leave (required)", "start_date": "2026-03-10 (required, YYYY-MM-DD)", "end_date": "2026-03-14 (required, YYYY-MM-DD)", "total_hours": 40, "notes": "Family holiday (optional)"}POST /api/leave/requests/:id/approve
Section titled “POST /api/leave/requests/:id/approve”Approve a pending leave request. Manager approval advances to pending_exec. Executive approval triggers integrations.
Permission: update:leave
Auth checks:
pending_manager→ approver must be squad manager or executivepending_exec→ approver must be executive
POST /api/leave/requests/:id/reject
Section titled “POST /api/leave/requests/:id/reject”Reject a pending leave request.
Permission: update:leave
Body:
{ "reason": "string (required)"}POST /api/leave/requests/:id/cancel
Section titled “POST /api/leave/requests/:id/cancel”Cancel an approved or pending leave request. If the request was approved, cancellation reverses all external integrations (Remote time-off cancelled, Productive booking deleted, Calendar event deleted, Slack cancellation posted).
Permission: update:leave (own requests or executive)
POST /api/leave/requests/:id/retry-integrations
Section titled “POST /api/leave/requests/:id/retry-integrations”Retry failed integrations for an approved request. Clears previous errors and re-runs all integrations.
Permission: manage:leave (Executive only)
GET /api/leave/pending-count
Section titled “GET /api/leave/pending-count”Get the number of leave requests pending the current user’s approval. Used for the sidebar badge.
Permission: view:leave
Response: { "count": 3 }
POST /api/admin/leave/backfill-from-xero
Section titled “POST /api/admin/leave/backfill-from-xero”One-shot rollout tool: pulls approved LeaveApplications from Xero (Payroll AU) into leave_requests. Idempotent — re-running is safe (dedupes on xero_leave_application_id via the partial unique index).
Internally this paginates Xero’s tenant-wide GET /payroll.xro/1.0/LeaveApplications?page=N endpoint (100 records/page) and matches each EmployeeID against the people.xero_employee_id map. Applications for Xero employees not present in Nucleus (ex-employees, contractors, etc.) are counted as unmapped_employees and skipped.
Backfilled rows are inserted with status = 'exec_approved' and manager_approved_by/exec_approved_by set to the calling admin. Hours are summed across LeavePeriods.NumberOfUnits. Leave types are matched against the cached leave_types table; if a type is missing, the cache is refreshed once from Xero then retried.
Permission: manage:leave (Executive only)
Body (all optional):
{ "person_id": "person_abc", // limit to a single person (filters client-side) "since": "2025-01-01", // skip LeaveApplications with StartDate < since "dry_run": true // count what would import without writing}Response:
{ "pages_fetched": 3, "applications_seen": 247, "unmapped_employees": 14, "processed_people": 28, "imported": 47, "already_present": 113, "errors": [], "dry_run": false}Rate-limited internally to ~50 page fetches per minute (Xero Payroll AU caps tenants at 60/min). The sanity cap is 50 pages (≈ 5,000 LeaveApplications) — raise it in admin-leave-backfill.ts if you ever hit it.