How to Forecast Revenue with Unified's CRM API
January 10, 2026
Revenue forecasting looks like a math problem. In reality, it's a data consistency problem that shows up in product decisions.
Once you support more than one CRM, the meaning of a 'forecast' starts to vary. Deal stages imply different probabilities. Pipelines may override stage-level logic or ignore it entirely. Some systems treat probability as advisory. Others treat it as contractual. Even something as basic as 'expected close date' may be a first-class field in one CRM and a custom field in another.
For a PM, this creates friction fast:
- Can forecasts be compared across customers on different CRMs?
- Do probability changes reflect real pipeline movement, or just stage configuration quirks?
- Can automation safely update probabilities without breaking how a sales team actually works?
Many forecasting features quietly become CRM-specific. Logic gets embedded for Salesforce first, patched for HubSpot, and partially supported elsewhere. The result is a feature that works, but only under the right conditions.
Unified's CRM API is designed to remove that fragmentation. Deals, pipelines, stages, and probabilities are normalized into a single model across CRM providers. Core fields like amount, probability, and stage configuration behave consistently, while CRM-specific data remains accessible through metadata when needed.
This guide shows how to build a revenue forecasting module on top of that normalized layer—listing open opportunities, calculating weighted revenue, grouping forecasts by expected close date, and updating probabilities—without branching logic per CRM or rewriting your forecasting rules as you add new integrations.
Prerequisites
- Node.js v18+
- A Unified account with a CRM integration enabled (Salesforce, HubSpot, Zoho, etc.)
- Your Unified API key
- A customer CRM
connectionId(created after your customer authorizes via Unified's auth flow)
Step 1: Set up your project
mkdir revenue-forecast-demo
cd revenue-forecast-demo
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;
const sdk = new UnifiedTo({
security: { jwt: UNIFIED_API_KEY! },
});
Step 3: Understand the normalized CRM objects
Unified returns normalized CRM objects in the SDK using snake_case field names.
Deals (CrmDeal)
A deal includes fields like:
id,name,amount,currencyclosed_at(the date the deal actually closed)stage,stage_id,pipeline,pipeline_idprobabilityuser_id,contact_ids,company_idsmetadata[](normalized access to custom fields)raw(integration-specific payload when needed)
Pipelines (CrmPipeline)
A pipeline includes:
id,name,is_active,display_orderdeal_probability(default probability at pipeline level)stages[], where each stage can include:deal_probabilityis_closed(whether the stage is a closed stage)active,display_order
Step 4: List open opportunities (deals)
Unified's SDK lists deals via:
GET /crm/{connection_id}/deal → sdk.crm.listCrmDeals(...)
It supports pagination (limit, offset) and filtering (e.g. pipeline_id, user_id, company_id, etc.).
This function fetches all open deals by paging through results, then filtering out closed deals (anything with closed_at set).
type CrmDeal = {
id?: string;
amount?: number;
probability?: number;
closed_at?: string; // actual close date
stage_id?: string;
pipeline_id?: string;
metadata?: { slug?: string; value?: any }[];
};
export async function listOpenDeals(
connectionId: string,
opts?: {
pipelineId?: string;
ownerId?: string;
pageSize?: number;
}
): Promise<CrmDeal[]> {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const openDeals: CrmDeal[] = [];
while (true) {
const deals = await sdk.crm.listCrmDeals({
connectionId,
limit: pageSize,
offset,
pipeline_id: opts?.pipelineId,
user_id: opts?.ownerId,
});
if (!deals || deals.length === 0) break;
openDeals.push(...deals.filter((d) => !d.closed_at));
offset += pageSize;
}
return openDeals;
}
Step 5: Calculate weighted revenue (amount × probability)
Weighted revenue is the expected value of the pipeline:
sum(amount × probability)
Unified's normalized CrmDeal includes both amount and probability, so you can compute this consistently across CRM vendors.
export function calculateWeightedRevenue(deals: CrmDeal[]): number {
return deals.reduce((total, deal) => {
const amount = Number(deal.amount ?? 0);
const probability = Number(deal.probability ?? 0);
return total + amount * probability;
}, 0);
}
Step 6: Group forecast by expected close month (using metadata)
Important detail: closed_at is the date the deal actually closed, so it won't exist for open deals. CRMs usually store an expected close date as a standard or custom field, which may not be part of Unified's normalized CrmDeal fields.
Unified exposes these extra fields through metadata[]. If your CRM stores expected close date as a field (often close_date, expected_close_date, etc.), you can group forecasts by month using the matching metadata.slug.
Helper: fetch a metadata value by slug
function getMetadataValue(deal: CrmDeal, slug: string): any {
return deal.metadata?.find((m) => m.slug === slug)?.value;
}
function toMonthKey(dateStr: string): string | null {
// Accept ISO timestamps or YYYY-MM-DD strings
if (!dateStr) return null;
const s = String(dateStr);
if (s.length < 7) return null;
return s.substring(0, 7); // YYYY-MM
}
Group by expected close month
export function groupForecastByExpectedCloseMonth(
deals: CrmDeal[],
expectedCloseDateSlug = 'close_date' // replace with your actual slug if different
): Record<string, number> {
return deals.reduce((acc, deal) => {
const expectedClose = getMetadataValue(deal, expectedCloseDateSlug);
const monthKey = toMonthKey(expectedClose);
if (!monthKey) return acc;
const amount = Number(deal.amount ?? 0);
const probability = Number(deal.probability ?? 0);
const weighted = amount * probability;
acc[monthKey] = (acc[monthKey] || 0) + weighted;
return acc;
}, {} as Record<string, number>);
}
If you don't know the right slug yet, the fastest way to confirm is to inspect one returned deal's metadata[] in your logs and pick the slug that corresponds to expected close date.
Step 7: List pipelines (optional)
Pipelines are useful if you want to:
- show pipeline options for filtering
- inspect stage configuration (
deal_probability, closed stages, ordering)
Use:
GET /crm/{connection_id}/pipeline → sdk.crm.listCrmPipelines(...)
export type CrmPipeline = {
id?: string;
name?: string;
is_active?: boolean;
deal_probability?: number;
display_order?: number;
stages?: {
id?: string;
name?: string;
active?: boolean;
deal_probability?: number;
is_closed?: boolean;
display_order?: number;
}[];
};
export async function listPipelines(connectionId: string): Promise<CrmPipeline[]> {
let offset = 0;
const pipelines: CrmPipeline[] = [];
while (true) {
const page = await sdk.crm.listCrmPipelines({
connectionId,
limit: 100,
offset,
});
if (!page || page.length === 0) break;
pipelines.push(...page);
offset += 100;
}
return pipelines;
}
Step 8: Update a deal probability (optional)
If your product allows users (or automation) to override probability, Unified supports updating a deal with:
PUT /crm/{connection_id}/deal/{id} → sdk.crm.updateCrmDeal(...)
export async function updateDealProbability(
connectionId: string,
dealId: string,
probability: number
): Promise<void> {
await sdk.crm.updateCrmDeal({
connectionId,
id: dealId,
crmDeal: {
probability,
},
});
}
Step 9: Putting it all together
This example fetches open deals, computes total weighted revenue, and groups the forecast by expected close month using a metadata slug.
async function main() {
const connectionId = CONNECTION_CRM!;
const expectedCloseDateSlug = 'close_date'; // replace if your slug differs
const openDeals = await listOpenDeals(connectionId, {
// pipelineId: '...',
// ownerId: '...',
pageSize: 100,
});
const totalWeighted = calculateWeightedRevenue(openDeals);
console.log('Total weighted revenue:', totalWeighted);
const forecastByMonth = groupForecastByExpectedCloseMonth(
openDeals,
expectedCloseDateSlug
);
console.log('Forecast by expected close month:', forecastByMonth);
// Optional: pipeline exploration
const pipelines = await listPipelines(connectionId);
console.log('Pipelines:', pipelines.map((p) => p.name));
}
main().catch(console.error);
Unified's CRM API gives you a consistent deal and pipeline model across CRM vendors, so you can build forecasting logic once and apply it across Salesforce, HubSpot, Zoho and more.
Key takeaways from this implementation:
- Use
sdk.crm.listCrmDeals(...)withlimitandoffsetto page through all deals. - Filter open deals using
closed_at(actual close timestamp). - Calculate weighted revenue using
amount × probability. - Group by expected close month using a close-date field exposed via
metadata[]. - Use
sdk.crm.updateCrmDeal(...)to update probabilities when needed.