Skip to content

Leave API

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

Force-refresh balances from the payroll provider (Xero or Remote), bypassing the 1hr cache.

Permission: update:leave

Response: Same as GET /api/leave/balances.

List leave requests scoped by access level and tab.

Permission: view:leave

Query params:

  • tabmine (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 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"
}
]
}

Submit a new leave request. Initial status depends on the submitter’s access level:

  • Executiveapproved (auto-approved, integrations run immediately)
  • Manager/Headpending_exec
  • Lead/Employeepending_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)"
}

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 executive
  • pending_exec → approver must be executive

Reject a pending leave request.

Permission: update:leave

Body:

{
"reason": "string (required)"
}

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 the number of leave requests pending the current user’s approval. Used for the sidebar badge.

Permission: view:leave

Response: { "count": 3 }

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.