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[] rawfor CRM-specific context if needed
Closed deals are identified by the presence of closed_at.
Pipelines (CrmPipeline)
Pipelines provide stage-level semantics:
stages[]with:idnamedeal_probabilityis_closedactivedisplay_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.