Unified.to
All articles

How to Analyze Compensation with Unified's HRIS API


January 10, 2026

Compensation analysis looks straightforward until you support more than one HR system.

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, it's often exposed inconsistently or buried behind vendor-specific assumptions.

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's HRIS API is designed to remove that fragmentation. It exposes employees and their current compensation packages through a normalized Employee model, regardless of which HRIS your customer uses. Compensation components, employment status, and core demographics are structured consistently, while leaving normalization policy decisions (like annualization or inclusion rules) explicitly in your control.

This guide shows how to build a compensation analysis module on top of that model—fetching employees, filtering active staff, normalizing compensation frequencies, and computing totals and averages—without maintaining vendor-specific connectors or embedding undocumented assumptions in your product logic.

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: Understand how compensation is represented in Unified

Unified exposes compensation data through the Employee object. There is no separate 'Employment' or 'Compensation' endpoint—everything you need for this use case is embedded in the employee record.

Employee (HrisEmployee)

Each employee includes:

  • Identity
    id, first_name, last_name, emails, title
  • Employment status
    employment_status (ACTIVE / INACTIVE)
    hired_at, terminated_at
    employment_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
    (Availability varies by HRIS and customer configuration.)

Compensation entries

Each item in compensation[] includes:

  • typeSALARY, BONUS, STOCK_OPTIONS, EQUITY, OTHER
  • amount — numeric value
  • currency — ISO currency code (e.g. USD)
  • frequencyONE_TIME, DAY, QUARTER, YEAR, HOUR, MONTH, WEEK
  • group_id — optional reference to an HRIS group (e.g. department)

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's HRIS API, compensation analysis becomes a straightforward data problem instead of a vendor-specific integration challenge. By relying on the normalized Employee model and its embedded compensation records, you can compute totals, averages, and demographic breakdowns across HRIS platforms using the same code path.

This approach provides:

  • Real-time access to current compensation
  • Currency-safe aggregation
  • Explicit, policy-controlled normalization
  • A clean foundation for FP&A, HR analytics, and pay-equity tooling

From here, common extensions include grouping by department or location, incorporating benefits and deductions for total-rewards analysis, or layering in historical tracking using HRIS-specific metadata when available.

All articles