---
title: "How to Forecast Revenue with Unified's CRM API"
img: https://s3.us-east-2.amazonaws.com/unified-article-images/how_to_forecast_revenue_with_unified_crm_api-icon.png
date: 2026-01-10T00:00:00.000Z
tag: Product
description: "Revenue forecasting looks like a math problem. In reality, it's a data consistency problem that shows up in product decisions."
url: "https://unified.to/blog/how_to_forecast_revenue_with_unified_crm_api"
---

# 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](/crm) 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](/technology) key
- A customer CRM `connectionId` (created after your customer authorizes via Unified's auth flow)

## Step 1: Set up your project


```bash
mkdir revenue-forecast-demo
cd revenue-forecast-demo
npm init -y
npm install @unified-api/typescript-sdk dotenv
```


Create a `.env` file:


```plain text
UNIFIED_API_KEY=your_unified_api_key
CONNECTION_CRM=your_customer_crm_connection_id
```


## Step 2: Initialize the SDK


```typescript
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`, `currency`
- `closed_at` (the date the deal actually closed)
- `stage`, `stage_id`, `pipeline`, `pipeline_id`
- `probability`
- `user_id`, `contact_ids`, `company_ids`
- `metadata[]` (normalized access to custom fields)
- `raw` (integration-specific payload when needed)

### Pipelines (`CrmPipeline`)


A pipeline includes:

- `id`, `name`, `is_active`, `display_order`
- `deal_probability` (default probability at pipeline level)
- `stages[]`, where each stage can include:
    - `deal_probability`
    - `is_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](/docs/reference/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).


```typescript
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.


```typescript
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


```typescript
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


```typescript
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(...)`


```typescript
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(...)`


```typescript
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.


```typescript
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(...)` with `limit` and `offset` to 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.

→ [Start your 30-day free trial](https://app.unified.to/login)


→ [Book a demo](https://calendly.com/d/cph9-g8n-jzg/connect-with-unified)