How to Analyze Employee Compensation Across HRIS Platforms with
·
Unified.to's HR & Directory API returns each employee's current pay as a list of compensation entries (type, amount, currency, frequency) on supported integrations, including Workday, BambooHR, ADP Workforce Now, SAP SuccessFactors and UKG Pro HCM (as of 19 October 2026). This guide is for product and engineering teams building compensation analytics, pay-equity or FP&A features: it shows how to read those entries, annualize them under your own rules, and total them per employee and per role without one integration per HRIS.
Across HRIS platforms, 'employee pay' is modeled very differently. Some systems emphasize salary. Others break compensation into many components. Employment status, contractor treatment, and demographic fields vary in both structure and availability. Even when the data exists, each vendor shapes it differently.
For a PM, this creates immediate ambiguity:
- Does 'total compensation' include bonuses, equity, or only base pay?
- Are we analyzing active employees only, or also recent terminations?
- Can averages and equity analyses be trusted across HRIS providers?
Many products handle this by hardcoding assumptions per vendor or narrowing scope to a single HR system. That works until customers ask for broader support or expect the numbers to line up across tools.
Unified.to's HR & Directory API returns employees and their current compensation in one Employee model. Compensation entries come back on supported integrations only, such as Workday, BambooHR, ADP Workforce Now, Ceridian Dayforce, SAP SuccessFactors and UKG Pro HCM; HiBob, Personio and Deel return employees without them (as of 19 October 2026). Check each integration's Feature Support tab before you promise the feature. Compensation components, employment status, and core demographics are structured consistently, while leaving normalization policy decisions (like annualization or inclusion rules) explicitly in your control.
Prerequisites
- Node.js v18+
- A Unified account with an HRIS integration enabled
- Your Unified API key (JWT)
- A customer HRIS
connectionId
Step 1: Set up your project
mkdir compensation-analysis-demo
cd compensation-analysis-demo
npm init -y
npm install @unified-api/typescript-sdk dotenv
Create a .env file:
UNIFIED_API_KEY=your_unified_api_key
CONNECTION_HRIS=your_customer_hris_connection_id
Step 2: Initialize the SDK
import 'dotenv/config';
import { UnifiedTo } from '@unified-api/typescript-sdk';
const { UNIFIED_API_KEY, CONNECTION_HRIS } = process.env;
const sdk = new UnifiedTo({
security: { jwt: UNIFIED_API_KEY! },
});
Step 3: Where does compensation live in Unified.to's Employee model?
On the Employee object. There's no separate compensation endpoint: current pay sits in each employee's compensation[] array. Pay actually paid lives elsewhere, in payslips (see How to Read Employee Payroll History), and benefits and deductions have their own objects.
Employee (HrisEmployee)
Each employee includes:
- Identity
id,first_name,last_name,emails,title - Employment status
employment_status(ACTIVE/INACTIVE)hired_at,terminated_atemployment_type(FULL_TIME,PART_TIME,CONTRACTOR, etc.) - Compensation array (
compensation[])
Each entry represents one component of the employee's compensation package. - Optional demographics
gender,date_of_birth,marital_status,pronouns
(Returned on some integrations only, e.g. gender and date of birth on Workday, BambooHR, HiBob and SAP SuccessFactors; pronouns on very few.)
Compensation entries
Each item in compensation[] includes:
type:SALARY,HOURLY,BONUS,COMMISSION,STOCK_OPTIONS,EQUITY,OTHERamount: numeric valuecurrency: ISO currency code (e.g.USD)frequency:ONE_TIME,HOUR,DAY,WEEK,BIWEEKLY,SEMIMONTHLY,MONTH,QUARTER,YEARgroup_id: optional reference to an HRIS group (e.g. department)notes: free text
Important constraints:
- Compensation entries represent the current state, not an effective-dated history.
- There are no start/end dates on compensation records.
- Multiple entries can exist per employee to represent different pay components (salary + bonus + equity).
Step 4: Fetch all employees (with pagination)
type HrisEmployee = {
id?: string;
title?: string;
employment_status?: 'ACTIVE' | 'INACTIVE';
terminated_at?: string | null;
employment_type?: string;
gender?: string;
compensation?: {
type?: 'SALARY' | 'BONUS' | 'STOCK_OPTIONS' | 'EQUITY' | 'OTHER';
amount?: number;
currency?: string;
frequency?: 'ONE_TIME' | 'DAY' | 'QUARTER' | 'YEAR' | 'HOUR' | 'MONTH' | 'WEEK';
}[];
};
export async function fetchAllEmployees(
connectionId: string,
pageSize = 100
): Promise<HrisEmployee[]> {
let offset = 0;
const employees: HrisEmployee[] = [];
while (true) {
const page = await sdk.hris.listHrisEmployees({
connectionId,
limit: pageSize,
offset,
fields: 'id,title,employment_status,terminated_at,employment_type,gender,compensation'
});
if (!page || page.length === 0) break;
employees.push(...page);
offset += pageSize;
}
return employees;
}
Step 5: Filter active employees
export function filterActiveEmployees(
employees: HrisEmployee[]
): HrisEmployee[] {
return employees.filter(
(e) => e.employment_status === 'ACTIVE' && !e.terminated_at
);
}
Step 6: Normalize compensation amounts (policy-dependent)
Compensation entries use different frequencies. To aggregate meaningfully, normalize amounts to a common time basis (for example, annualized).
Note: The multipliers below are defaults. You should adjust them to match your organization's working-hour and working-day policies.
const frequencyMultiplier: Record<string, number> = {
YEAR: 1,
QUARTER: 4,
MONTH: 12,
WEEK: 52,
DAY: 260, // policy assumption
HOUR: 2080, // policy assumption
ONE_TIME: 1,
};
function normalizeAmount(amount: number, frequency: string): number {
return amount * (frequencyMultiplier[frequency] ?? 1);
}
Step 7: Aggregate compensation per employee (currency-safe)
Instead of aggregating per compensation record, first compute total compensation per employee, grouped by currency.
type EmployeeCompTotal = {
employee_id: string;
title?: string;
employment_type?: string;
gender?: string;
totals_by_currency: Record<string, number>;
};
export function aggregatePerEmployee(
employees: HrisEmployee[]
): EmployeeCompTotal[] {
const results: EmployeeCompTotal[] = [];
for (const e of employees) {
if (!e.id) continue;
const totals: Record<string, number> = {};
for (const c of e.compensation ?? []) {
if (c.amount == null || !c.currency || !c.frequency) continue;
const normalized = normalizeAmount(c.amount, c.frequency);
totals[c.currency] = (totals[c.currency] ?? 0) + normalized;
}
results.push({
employee_id: e.id,
title: e.title,
employment_type: e.employment_type,
gender: e.gender,
totals_by_currency: totals,
});
}
return results;
}
Step 8: Compute totals and averages
Total compensation (by currency)
export function totalCompensationByCurrency(
employees: EmployeeCompTotal[]
): Record<string, number> {
const totals: Record<string, number> = {};
for (const e of employees) {
for (const [currency, amount] of Object.entries(e.totals_by_currency)) {
totals[currency] = (totals[currency] ?? 0) + amount;
}
}
return totals;
}
Average pay by role (per employee)
export function averagePayByRole(
employees: EmployeeCompTotal[]
): Record<string, Record<string, number>> {
const totals: Record<string, Record<string, number>> = {};
const counts: Record<string, number> = {};
for (const e of employees) {
const role = e.title ?? 'Unknown';
counts[role] = (counts[role] ?? 0) + 1;
for (const [currency, amount] of Object.entries(e.totals_by_currency)) {
totals[role] ??= {};
totals[role][currency] = (totals[role][currency] ?? 0) + amount;
}
}
const averages: Record<string, Record<string, number>> = {};
for (const role of Object.keys(totals)) {
averages[role] = {};
for (const [currency, total] of Object.entries(totals[role])) {
averages[role][currency] = total / counts[role];
}
}
return averages;
}
Optional: breakdown by demographic field
export function breakdownByField(
employees: EmployeeCompTotal[],
field: 'gender' | 'employment_type'
): Record<string, Record<string, number>> {
const totals: Record<string, Record<string, number>> = {};
for (const e of employees) {
const key = (e as any)[field] ?? 'Unknown';
totals[key] ??= {};
for (const [currency, amount] of Object.entries(e.totals_by_currency)) {
totals[key][currency] = (totals[key][currency] ?? 0) + amount;
}
}
return totals;
}
Step 9: Putting it all together
async function main() {
const employees = await fetchAllEmployees(CONNECTION_HRIS!);
const active = filterActiveEmployees(employees);
const perEmployee = aggregatePerEmployee(active);
console.log('Total compensation by currency:');
console.log(totalCompensationByCurrency(perEmployee));
console.log('Average pay by role:');
console.log(averagePayByRole(perEmployee));
console.log('Compensation by gender:');
console.log(breakdownByField(perEmployee, 'gender'));
}
main().catch(console.error);
With Unified.to's HR & Directory API, the same code computes totals, averages and demographic breakdowns on every integration that returns compensation entries; on the rest, your module should show that pay data isn't available rather than a zero.
This approach provides:
- Current compensation, read from the HRIS at request time (no customer records at rest on Unified.to)
- Currency-safe aggregation
- Explicit, policy-controlled normalization
- A clean foundation for FP&A, HR analytics, and pay-equity tooling
From here, common extensions are grouping by department or location (groups, locations), adding benefits and deductions for total-rewards views on integrations that return them, and reading pay history from payslips.
Related: How to Read Employee Payroll History · HR API Integration · Employee data model