Unified.to
All articles

How to Rank and Recommend Sales Opportunities Using Unified's CRM API


February 2, 2026

Opportunity recommendations only work if the signals behind them are consistent.

Across CRMs, deals don't mean the same thing. Stages imply different probabilities. Pipelines encode different business processes. Some teams update probability manually. Others rely on stage defaults. Activity may be attached to a deal directly, inferred from contacts, or logged inconsistently.

If you try to rank opportunities without accounting for those differences, recommendations become hard to trust. A deal looks 'hot' in one CRM and stale in another, even when the underlying signals are similar.

Unified's CRM API gives you a normalized deal, pipeline, and event model that makes opportunity ranking explicit and explainable. You can list open deals, attach stage semantics from pipelines, incorporate recent activity from events, and produce ranked recommendations using documented fields only.

This guide shows how to rank and recommend sales opportunities using Unified's CRM API in TypeScript. It focuses on deals and pipelines as the core ranking signal, with optional engagement signals from events. No UI assumptions, no webhooks, and no undocumented behavior.

Prerequisites

  • Node.js v18+
  • A Unified account with a CRM integration enabled
  • Your Unified API key
  • A customer CRM connectionId

Step 1: Set up your project

mkdir crm-opportunity-ranking
cd crm-opportunity-ranking
npm init -y
npm install @unified-api/typescript-sdk dotenv

Create a .env file:

UNIFIED_API_KEY=your_unified_api_key
CONNECTION_CRM=your_customer_crm_connection_id

Step 2: Initialize the SDK

import "dotenv/config";
import { UnifiedTo } from "@unified-api/typescript-sdk";

const { UNIFIED_API_KEY, CONNECTION_CRM } = process.env;

if (!UNIFIED_API_KEY) throw new Error("Missing UNIFIED_API_KEY");
if (!CONNECTION_CRM) throw new Error("Missing CONNECTION_CRM");

const sdk = new UnifiedTo({
  security: { jwt: UNIFIED_API_KEY },
});

const connectionId = CONNECTION_CRM;

Step 3: Understand the ranking inputs

Opportunity ranking is built from three documented CRM objects.

Deals (CrmDeal)

Deals are the unit being ranked. Fields commonly used for ranking include:

  • identity and timing: id, created_at, updated_at
  • value: amount, currency
  • pipeline context: stage, stage_id, pipeline, pipeline_id
  • lifecycle: closing_at, closed_at
  • probability: probability (number; range not defined)
  • associations: contact_ids[], company_ids[]
  • attribution: user_id
  • custom fields: metadata[]
  • raw for CRM-specific context if needed

Closed deals are identified by the presence of closed_at.

Pipelines (CrmPipeline)

Pipelines provide stage-level semantics:

  • stages[] with:
    • id
    • name
    • deal_probability
    • is_closed
    • active
    • display_order

This lets you interpret stages consistently without hardcoding CRM-specific logic.

Events (CrmEvent) optional

Events represent recent activity tied to deals:

  • type: CALL, MEETING, EMAIL, NOTE, TASK, etc.
  • relationship field: deal_ids[]
  • timestamps: created_at, updated_at

Events can be used as a recency or engagement signal, but they are optional. The core ranking works without them.

Step 4: Fetch open deals

Opportunity recommendations should focus on deals that are still open.

async function fetchOpenDeals(opts?: {
  pageSize?: number;
  updated_gte?: string;
  pipeline_id?: string;
}) {
  const pageSize = opts?.pageSize ?? 100;
  let offset = 0;
  const out: any[] = [];

  while (true) {
    const page = await sdk.crm.listCrmDeals({
      connectionId,
      limit: pageSize,
      offset,
      updated_gte: opts?.updated_gte ?? "",
      pipeline_id: opts?.pipeline_id ?? "",
      sort: "updated_at",
      order: "asc",
    });

    if (!page || page.length === 0) break;

    for (const d of page) {
      if (!d.closed_at) {
        out.push(d);
      }
    }

    if (page.length < pageSize) break;
    offset += pageSize;
  }

  return out;
}

This excludes closed deals using the documented closed_at field.

Step 5: Fetch pipelines and stage metadata

Stage metadata is required to interpret where a deal sits in the sales process.

async function fetchAllPipelines() {
  let offset = 0;
  const out: any[] = [];

  while (true) {
    const page = await sdk.crm.listCrmPipelines({
      connectionId,
      limit: 100,
      offset,
      sort: "updated_at",
      order: "asc",
    });

    if (!page || page.length === 0) break;

    out.push(...page);
    if (page.length < 100) break;
    offset += 100;
  }

  return out;
}

function indexStagesByPipeline(pipelines: any[]) {
  const out: Record<string, Record<string, any>> = {};

  for (const p of pipelines) {
    if (!p?.id) continue;
    out[p.id] = {};
    for (const s of p.stages ?? []) {
      if (!s?.id) continue;
      out[p.id][s.id] = s;
    }
  }

  return out;
}

Step 6: Define a ranking score

Ranking should be explainable. Avoid opaque formulas that depend on undocumented behavior.

Here's a conservative scoring model:

  • Base score from deal value and probability
  • Stage weight from pipeline stage metadata
  • Optional boost from recent activity
function scoreDeal(input: {
  deal: any;
  stage?: any;
  recentEventCount?: number;
}) {
  const amount = Number(input.deal.amount ?? 0);
  const probability = Number(input.deal.probability ?? 0);

  const baseScore = amount * probability;

  const stageProbability =
    typeof input.stage?.deal_probability === "number"
      ? input.stage.deal_probability
      : 1;

  const activityBoost =
    typeof input.recentEventCount === "number"
      ? 1 + Math.min(input.recentEventCount, 5) * 0.05
      : 1;

  return baseScore * stageProbability * activityBoost;
}

This model:

  • does not assume a probability range
  • does not mix currencies
  • uses stage metadata only when available
  • treats engagement as a bounded boost

Step 7: Optional engagement signal from events

If you want to boost deals with recent activity, fetch events tied to each deal.

async function countRecentDealEvents(dealId: string, updated_gte: string) {
  let offset = 0;
  let count = 0;

  while (true) {
    const page = await sdk.crm.listCrmEvents({
      connectionId,
      limit: 100,
      offset,
      updated_gte,
      deal_id: dealId,
      sort: "updated_at",
      order: "asc",
    });

    if (!page || page.length === 0) break;

    count += page.length;
    if (page.length < 100) break;
    offset += 100;
  }

  return count;
}

Use updated_at and updated_gte as the recency checkpoint.

Step 8: Rank opportunities

async function rankOpportunities() {
  const deals = await fetchOpenDeals();
  const pipelines = await fetchAllPipelines();
  const stagesByPipeline = indexStagesByPipeline(pipelines);

  const ranked = [];

  for (const d of deals) {
    const stage =
      d.pipeline_id && d.stage_id
        ? stagesByPipeline[d.pipeline_id]?.[d.stage_id]
        : undefined;

    const score = scoreDeal({
      deal: d,
      stage,
    });

    ranked.push({
      deal_id: d.id,
      name: d.name,
      stage: d.stage,
      pipeline: d.pipeline,
      amount: d.amount,
      currency: d.currency,
      score,
    });
  }

  ranked.sort((a, b) => b.score - a.score);
  return ranked;
}

This produces an ordered list of opportunities you can surface to users.

Step 9: Present recommendations

Unified provides the ranking signal. Presentation happens elsewhere.

You can now:

  • show the top N deals per rep
  • trigger reminders for high-scoring opportunities
  • generate daily or weekly follow-up queues
  • audit why a deal was recommended using score components

Summary

Using Unified's CRM API, you can rank and recommend sales opportunities with a clear, explainable data model:

  • Fetch open deals using documented lifecycle fields
  • Interpret stage semantics through pipelines
  • Optionally incorporate recent activity from events
  • Score and sort opportunities without CRM-specific branching
  • Keep recommendations auditable and portable across integrations

By grounding recommendations in normalized CRM objects and explicit constraints, you avoid the ambiguity that makes opportunity ranking hard to trust as you support more CRMs.

Start your 30-day free trial

Book a demo

All articles