@markwharton/eh-hr
v3.1.0
Published
Employment Hero HR API client
Downloads
1,306
Readme
@markwharton/eh-hr
Employment Hero HR API client for employees, teams, and leave requests.
Install
npm install @markwharton/eh-hrQuick Start
import { EHClient, getAccessToken } from '@markwharton/eh-hr';
// Step 1: Obtain an access token via OAuth2
const tokenResult = await getAccessToken({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
refreshToken: 'your-refresh-token',
});
if (!tokenResult.ok) throw new Error(tokenResult.error);
// Step 2: Persist rotated refresh token if returned
if (tokenResult.data!.refreshToken) {
await saveRefreshToken(tokenResult.data!.refreshToken);
}
// Step 3: Create the client with the access token
const client = new EHClient({
accessToken: tokenResult.data!.accessToken,
organisationId: '123',
cache: {}, // enable caching with defaults
retry: {}, // enable retry with defaults
});
// Step 4: Validate the token works
const validation = await client.validateToken();
if (!validation.ok) throw new Error(validation.error);
// Get all employees (auto-paginates)
const empResult = await client.getEmployees();
if (empResult.ok) console.log(empResult.data); // EHEmployee[]
// Get a single employee by ID
const singleResult = await client.getEmployee('emp-123');
if (singleResult.ok) console.log(singleResult.data); // EHEmployee
// Filter by status (client-side, case-insensitive)
const active = await client.getEmployees({ status: 'active' });
// Get all teams
const teamResult = await client.getTeams();
if (teamResult.ok) console.log(teamResult.data); // EHTeam[]
// Get leave requests for a date range (server-side filtering)
const leaveResult = await client.getLeaveRequests({
startDate: '2026-01-01',
endDate: '2026-03-31',
employeeId: '150', // client-side filter
});
if (leaveResult.ok) console.log(leaveResult.data); // EHLeaveRequest[]Result Pattern
All methods return Result<T> — see api-core Result Pattern. Always check ok before accessing data.
Authentication
OAuth2 Refresh Token Grant
The HR API uses OAuth2 Bearer token authentication. The Quick Start above shows the full flow: getAccessToken() → persist rotated token → create client → validate.
Under the hood, getAccessToken() sends:
POST https://oauth.employmenthero.com/oauth2/token
Content-Type: application/x-www-form-urlencoded
client_id=xxx&client_secret=yyy&grant_type=refresh_token&refresh_token=zzzThe response is mapped from snake_case to camelCase:
| API field | EHTokenResponse field | Description |
|-----------|------------------------|-------------|
| access_token | accessToken | Token for API requests |
| refresh_token | refreshToken | New refresh token (if rotated) |
| token_type | tokenType | Typically 'Bearer' |
| expires_in | expiresIn | Lifetime in seconds |
| scope | scope | OAuth2 scope |
Refresh token rotation: The auth server may return a new refresh token. If refreshToken is present in the response, persist it — the old one may be invalidated. The library does not handle token storage, rotation persistence, or expiry tracking; that is the consumer's responsibility.
Custom auth URL: Pass a second argument to override the default auth endpoint:
const tokenResult = await getAccessToken(credentials, 'https://custom-auth.example.com/token');Validating the Token
The HR API has no dedicated auth-check endpoint. validateToken() verifies the access token by requesting a single employee record (page_index=1, item_per_page=1). Returns 401 or 403 for invalid/expired tokens.
API Reference
| Method | Parameters | Returns |
|--------|-----------|---------|
| validateToken() | — | Result<void> |
| getEmployees(options?) | EHEmployeeOptions? | Result<EHEmployee[]> |
| getEmployee(employeeId) | string | Result<EHEmployee> |
| getTeams() | — | Result<EHTeam[]> |
| getLeaveRequests(options?) | EHLeaveRequestOptions? | Result<EHLeaveRequest[]> |
| clearCache() | — | void |
| invalidateEmployeeCache() | — | void |
| invalidateLeaveRequestCache() | — | void |
| invalidateTeamCache() | — | void |
Options
EHEmployeeOptions:
| Option | Type | Description |
|--------|------|-------------|
| memberType | 'employee' \| 'contractor' | Filter by member type (server-side) |
| employeeId | string | Filter by employee code or ID (client-side, matches code or id) |
| status | string | Filter by status, e.g. 'active' or 'inactive' (client-side, case-insensitive) |
| pageSize | number | Internal batch size for auto-pagination (default: 100) |
EHLeaveRequestOptions:
| Option | Type | Description |
|--------|------|-------------|
| startDate | string | Filter by start date, YYYY-MM-DD (server-side) |
| endDate | string | Filter by end date, YYYY-MM-DD (server-side) |
| employeeId | string | Filter by employee code or UUID (client-side, resolves code → UUID via getEmployees) |
| status | string | Filter by status, e.g. 'approved', 'pending' (client-side, case-insensitive) |
| pageSize | number | Internal batch size for auto-pagination (default: 100) |
Auto-Pagination
All data methods automatically paginate through all results using the HR API's page_index/item_per_page parameters. The consumer always receives the complete array.
The pageSize option controls the internal batch size (items per API call, default 100):
// Fetch employees in smaller batches (50 per API call)
const result = await client.getEmployees({ pageSize: 50 });Server-Side and Client-Side Filtering
Some filters are applied server-side (reducing data transferred), while others are applied client-side after cache retrieval:
- Server-side:
memberType(employees),startDate/endDate(leave requests) — passed as query parameters to the API. Different filter values produce separate cache entries. - Client-side:
employeeId,status— applied after fetching. All filter combinations for the same server-side query share a cache entry.
Leave requests are time-series data — unlike employees and teams which remain relatively stable, leave requests grow continuously as new requests accumulate. Always use startDate/endDate filters to bound the result set. Without date filters, the API returns the entire leave request history, which increases over time and may be significant in memory-constrained environments (e.g., serverless functions).
Configuration
Provide either accessToken (manual token lifecycle) or credentials (auto-managed). Not both.
Manual Token Mode
const client = new EHClient({
accessToken: 'xxx', // OAuth2 access token (manual lifecycle)
organisationId: '123',
cache: {},
retry: {},
});Credentials Mode (Auto-Managed)
const client = new EHClient({
credentials: { // OAuth2 credentials (auto-managed lifecycle)
clientId: 'xxx',
clientSecret: 'yyy',
refreshToken: 'zzz',
},
organisationId: '123',
onTokenRefresh: async (newRefreshToken) => {
await saveRefreshToken(newRefreshToken); // persist rotated token
},
cache: {},
retry: {},
});In credentials mode, the client automatically:
- Lazily acquires the token on first API call (not at construction)
- Caches the token and reuses it within its TTL
- Proactively refreshes at 80% of TTL (e.g., 48 min for a 60 min token)
- Coalesces concurrent refresh requests (only one auth call)
- Rotates refresh tokens — updates internally and calls
onTokenRefreshso you can persist
All Options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| accessToken | string | — | OAuth2 access token (manual mode) |
| credentials | EHOAuthCredentials | — | OAuth2 credentials (credentials mode) |
| organisationId | string | required | Organisation ID |
| authUrl | string | EH production auth URL | Override OAuth2 token endpoint |
| onTokenRefresh | (token: string) => void | — | Callback when refresh token is rotated |
| baseUrl | string | EH HR API URL | Override API base URL |
| onRequest | callback | — | Debug callback for each API request |
| rateLimitPerSecond | number | 5 | Client-side rate limiting (0 to disable) |
| cache | EHCacheConfig | disabled | Enable caching with optional TTL overrides |
| retry | RetryConfig | disabled | Enable retry for 429/503 with exponential backoff |
EHConfig extends ClientConfig from api-core, which provides the baseUrl, onRequest, retry, and cacheInstance fields. Pass cacheInstance to use a custom cache backend (e.g., LayeredCache with persistent stores); otherwise cache: {} creates an in-memory TTLCache.
Cache TTLs
| Cache Key | Default TTL | |-----------|-------------| | Employees | 5 min | | Teams | 5 min | | Leave requests | 2 min |
Failed API results (ok: false) are never cached — transient errors won't persist for the full TTL. See the root README Cache System section for the full cache architecture (layered stores, restricted data handling, request coalescing).
Rate Limiting
The client enforces a sliding-window rate limit of 5 requests per second. All outbound requests pass through the rate limiter automatically. Set rateLimitPerSecond: 0 in config to disable (useful for tests). The RateLimiter class is provided by api-core and operates per-instance.
Retry
Automatically retries on HTTP 429 (Too Many Requests) and 503 (Service Unavailable) with exponential backoff. Respects the Retry-After header when present.
Utilities
| Export | Description |
|--------|-------------|
| getAccessToken(credentials, authUrl?) | OAuth2 refresh token grant helper |
| pickFields<T>(obj, spec) | Select fields from API responses (re-exported from api-core) |
| RateLimiter | Sliding window rate limiter class (re-exported from api-core) |
Error Handling
| Export | Description |
|--------|-------------|
| HRError | Custom error class extending ApiError, with status, static fromResponse() |
| parseHRErrorResponse | Parse EH HR error JSON to HRParsedError |
License
MIT
