npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-hr

Quick 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=zzz

The 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 onTokenRefresh so 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