Unified.to
All articles

How to Analyze Sales Rep Performance with Unified's CRM API


January 10, 2026

Sales performance features often start life as dashboards. They fail when the underlying data contract isn't stable.

Once you support multiple CRMs, 'rep performance' stops meaning one thing. Ownership can be a user, a team, or reassigned mid-deal. Deal status isn't always explicit. Probability may be inferred from stage, overridden manually, or ignored entirely. Historical context may or may not exist depending on how the CRM was configured.

For a PM, this creates sharp edges:

  • Can performance metrics be trusted across Salesforce, HubSpot, and Zoho?
  • Are reps being measured on closed revenue, pipeline, or a mix of both?
  • Can you explain why a number changed if the CRM didn't record history?

Many products quietly blur these distinctions. Metrics are computed from cached data, undocumented fields, or assumptions that only hold for one CRM. The result looks correct—until a customer asks how it was calculated.

Unified's CRM API is designed to make those boundaries explicit. It exposes a real-time, normalized view of deals and ownership, without storing snapshots or inventing state. What you compute is always based on the current CRM truth, using documented fields only.

This guide shows how to analyze sales rep performance using that model—attributing deals to reps, calculating revenue and weighted pipeline, and optionally enriching reps via HRIS—while staying honest about what can and cannot be computed without your own storage layer.

Prerequisites

  • Node.js 18+
  • A Unified account
  • An API key
  • A CRM connection_id
  • @unified-api/typescript-sdk (v2.82.14)
npm install @unified-api/typescript-sdk

Initialize the SDK

import { UnifiedTo } from '@unified-api/typescript-sdk';

const sdk = new UnifiedTo({
  security: {
    jwt: process.env.UNIFIED_API_KEY!,
  },
});

Step 1: Fetch Deals (Real-Time)

Unified's CRM API is real-time passthrough. Every request hits the source CRM live; there are no cached snapshots.

We'll fetch only the fields required for performance analysis.

const connectionId = 'YOUR_CONNECTION_ID';

const deals = await sdk.crm.listCrmDeals({
  connectionId,
  limit: 100,
  offset: 0,
  // Optional incremental fetch:
  // updated_gte: '2026-01-11T00:00:00Z',
  fields: [
    'id',
    'user_id',
    'amount',
    'currency',
    'probability',
    'closed_at',
    'closing_at',
    'stage',
    'pipeline',
    'won_reason',
    'lost_reason',
  ],
});

Notes

  • Returns a CrmDeal[] array (no cursor wrapper).
  • Pagination uses limit + offset.
  • Filtering by rep is supported via user_id.
  • There is no status filter; infer open/won/lost from fields.

Step 2: Attribute Deals to Reps

Each deal has user_id, which references an HRIS employee.

type Deal = typeof deals[number];

function filterDealsByRep(allDeals: Deal[], repId: string): Deal[] {
  return allDeals.filter(d => d.user_id === repId);
}

Step 3: Aggregate Revenue (Currency-Safe)

Unified does not convert currencies. Aggregate per rep per currency.

type RevenueByCurrency = Record<string, number>;
type RevenueByRep = Record<string, RevenueByCurrency>;

function aggregateRevenueByRep(allDeals: Deal[]): RevenueByRep {
  const result: RevenueByRep = {};

  for (const d of allDeals) {
    if (!d.user_id || typeof d.amount !== 'number' || !d.currency) continue;

    result[d.user_id] ??= {};
    result[d.user_id][d.currency] ??= 0;
    result[d.user_id][d.currency] += d.amount;
  }

  return result;
}

Step 4: Weighted Pipeline (Optional)

Use amount × probability for open deals.

type WeightedByRep = Record<string, RevenueByCurrency>;

function aggregateWeightedPipeline(allDeals: Deal[]): WeightedByRep {
  const result: WeightedByRep = {};

  for (const d of allDeals) {
    if (
      !d.user_id ||
      typeof d.amount !== 'number' ||
      typeof d.probability !== 'number' ||
      !d.currency ||
      d.closed_at // skip closed deals
    ) continue;

    const weighted = d.amount * d.probability;

    result[d.user_id] ??= {};
    result[d.user_id][d.currency] ??= 0;
    result[d.user_id][d.currency] += weighted;
  }

  return result;
}

Step 5: Resolve Rep Names (Optional, HRIS)

Deals only carry user_id. To show rep names/emails, resolve via HRIS.

Retrieve a Single Employee

const rep = await sdk.hris.getHrisEmployee({
  connectionId,
  id: 'EMPLOYEE_ID',
  fields: ['id', 'name', 'emails', 'title'],
});

Or List Employees (Bulk Join)

const employees = await sdk.hris.listHrisEmployees({
  connectionId,
  limit: 100,
  offset: 0,
  fields: ['id', 'name', 'emails'],
});

Build a lookup map by id and join to your aggregates.

What You Can Compute Reliably

Based on documented fields and real-time semantics:

  • Total revenue per rep (by currency)
  • Average deal size per rep
  • Deal counts per rep
  • Weighted pipeline (amount × probability)
  • Sales cycle length (for closed deals: closed_at - created_at)
  • Current pipeline distribution by stage

What You Cannot Compute (Without Your Own Storage)

These are constrained by documented behavior:

  • Stage velocity / conversion rates (no stage history)
  • Historical trends (no snapshots; current-state only)
  • Cross-currency rollups (no conversion)
  • Explicit status filtering (no status enum)

If you need these, persist deal snapshots on your side.

Summary

Using Unified's CRM API, you can analyze sales rep performance with live, normalized data:

  • Fetch deals in real time
  • Attribute to reps via user_id
  • Aggregate revenue and weighted pipeline safely
  • Optionally enrich reps via HRIS
All articles