# CMBS Loan Data API - CRE Loans, Comps, Financials (`nabeelbaghoor/cmbs-loan-data-api`) Actor

Securitised commercial real estate data: turn an address into a location ID, then read the loan on it, its servicer and watchlist status, valuations at origination and today, detailed financial statements by period and statement type, top tenants with lease expiries, and scored comparables.

- **URL**: https://apify.com/nabeelbaghoor/cmbs-loan-data-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Real estate, Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 location resolved or listeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## CMBS Loan Data API - CRE Loans, Comps, Financials

Look up any US commercial property that sits behind a securitised loan: resolve the address to a location ID, then read the loan on it, who services it and whether it is on a watchlist, what it was valued at when it was written and what it is valued at now, its financial statements period by period, its top tenants with their lease expiries, and the properties the provider scores as its closest comparables.

### What this actor does

- **Address to location ID.** Resolve a street address, a Plus Code or a Google Place ID to the provider's own location ID, with a confidence score and the data sources behind the record. Everything else keys off that ID.
- **The loan, not just the building.** Deal and loan identifiers, prospectus loan ID, borrower sponsor, master and special servicer, originator, lender and loan seller, original and current balance, rate and rate type, origination and maturity dates, payment status and loan purpose.
- **Distress, with its dates.** Servicer watchlist and special servicing each arrive as a flag and a date and a reason. Both travel: the flag says where the loan is now, the date says how long it has been there, and a loan that went on the watchlist four years ago is a different animal from one that went on last month.
- **Financial statements that keep their type.** The provider publishes the same period as normalised, adjusted and borrower reported figures. Those are three different claims about one year of trading, so the statement type and the period type sit on every row and nothing is averaged across them.
- **Top tenants as their own rows.** Name, normalised name, rank, size, share of gross and net rentable area, the date the tenancy was confirmed and the date the lease expires. One row per tenant, so an expiry is something you can filter on.
- **Scored comparables, with the reason for the score.** Loan, sale, property and financial comparables, each carrying the provider's similarity score, the distance to the subject, and the breakdown of which fields the score came from and how heavily each was weighted.
- **Saved lists, filters included.** Read a saved screen back out with the full filter set behind it, then expand it into the locations it currently resolves to.
- **Pay per result.** You are charged for rows carrying a real record. Identifiers the provider does not recognise and searches that return nothing are free.

### Bring your own key

This actor calls the provider with **your own credentials**. It accepts either form the API publishes: an **API key**, sent in the `x-api-key` header, or a **client ID and client secret**, exchanged once for an access token that is then cached until it expires rather than re-fetched on every request. Paste whichever you have into the input, or set the key as the `DATA_API_KEY` secret. Nothing runs on anyone else's credentials, and a run without them ends cleanly with an explanation rather than failing.

### "Missing Authentication Token" does not mean what it says

Worth knowing before it happens, because it is the single most misleading answer this API gives. The gateway returns `Missing Authentication Token` when **no route matches the path and method** - not when a credential is absent. A genuinely refused key answers `Forbidden` instead.

Verified against the live gateway while building this actor: real routes answered `Forbidden` with an invalid key, and an invented route answered `Missing Authentication Token` with exactly the same key. So this actor reports the two differently, and treats only the second as something wrong with your credentials. If you ever see the first, the problem is the request that was built, not the key you pasted.

### Statement types are not samples

A property's trading year is published more than once. The normalised statement has been adjusted to a common basis so it can be compared across buildings. The borrower reported statement is what the borrower filed. The adjusted statement sits between them. They disagree, and the disagreement is the point.

A pipeline that averages them produces a number nobody filed, and one that silently picks whichever came first will mix bases across a portfolio. So every financial row records its statement type and its period type, and the category codes inside a statement are written as their own columns under a `cat_` prefix rather than mapped onto names this actor would have had to invent. A category the provider did not report stays empty rather than becoming a zero.

### Financials come ten at a time

The detailed financials endpoint accepts at most ten locations per request, and it reports the ones it could not serve inside an otherwise successful response rather than by failing. This actor splits your list into tens automatically and turns every unserved location into its own row, so a location that returned nothing is visible in the output rather than silently absent from it.

It also requires a property type for the whole request, because the category codes inside a statement differ by type. Run a mixed portfolio once per property type.

### Example output

```json
{
  "recordType": "details",
  "requested": "1200 Market St, Philadelphia, PA",
  "found": true,
  "locationId": 1184402,
  "address": "1200 MARKET ST",
  "city": "PHILADELPHIA",
  "state": "PA",
  "msa": "Philadelphia-Camden-Wilmington, PA-NJ-DE-MD",
  "propertyTypes": "Office",
  "size": 863000,
  "sizeUnits": "SF",
  "yearBuilt": 1984,
  "occupancyPercent": 78.4,
  "occupancyAsOfDate": "2025-12-31",
  "dealName": "BANK 2019-BNK17",
  "debtLoanId": "BNK17-0012",
  "borrowerSponsor": "Example Holdings LP",
  "masterServicer": "Example Servicing LLC",
  "specialServicer": "Example Special Servicing LLC",
  "originalBalance": 92000000,
  "currentBalance": 88750000,
  "interestRate": 4.21,
  "interestRateType": "fixed",
  "originationDate": "2019-06-14",
  "maturityDate": "2029-07-01",
  "paymentStatus": "Current",
  "onServicerWatchlist": true,
  "watchlistDate": "2024-11-01",
  "watchlistReason": "DSCR below threshold",
  "inSpecialServicing": false,
  "dscrNoi": 1.08,
  "debtYieldNoi": 7.4,
  "tenantCount": 5
}
```

### FAQ

#### What CMBS and CRE loan data does this actor return?

For a property: the address, county and metro, property types and subtypes, size, units, year built and renovated, rent and occupancy with its as-of date. For the loan on it: deal name and type, loan and prospectus identifiers, borrower sponsor, master and special servicer, originator, lender, original and current balance, rate and rate type, origination and maturity dates, payment status, watchlist and special servicing dates and reasons, workout strategy, modification type and date, and defeasance, bankruptcy and paid-off flags. For its performance: income, operating expenses, debt service, NOI, NCF, DSCR and debt yield on both bases, plus valuations at origination and most recently.

#### How do I find a location ID from an address?

Run resolve mode with the address, a Plus Code or a Google Place ID. It returns the provider's location ID with a confidence score. Search mode does the same thing from a partial address and returns full records for every match, so it is the better starting point when you have a street and a city but not a full address.

#### Can I get CMBS loans on a servicer watchlist?

Yes. Every property record carries the watchlist flag, the date the loan was added, and the reason the servicer gave, alongside the special servicing transfer and return dates and the workout strategy. Filter the dataset on the flag, or sort on the date to see how long each loan has been sitting there.

#### What is the difference between normalised, adjusted and borrower reported financials?

They are three different statements of the same period rather than three measurements of it. Normalised figures have been put on a common basis so buildings can be compared. Borrower reported figures are what the borrower filed. Adjusted figures sit between the two. Each financial row records which one it is, so your comparisons stay on one basis.

#### How do comparables get scored?

The provider scores each candidate against the subject and returns the score, the distance, and a breakdown naming each field that contributed, the weight it carried and the raw value behind it. That breakdown travels on every comparable row, so a score of 0.91 can be read as "close on location and vintage" rather than taken on trust.

#### Which comparable type should I use?

Loan comparables compare debt terms, so they answer what a similar building borrowed at. Sale comparables compare transactions, with buyer, seller and prior sale. Property comparables compare size, vintage and type. Financial comparables compare trading performance: occupancy, average rent, revenues and expenses. Leave the type empty and the provider returns sale and loan comparables together.

#### What do the category codes in a financial statement mean?

They are the provider's own chart of accounts, and they differ by property type, which is why a financials run covers one property type at a time. This actor writes each reported code as its own column under a `cat_` prefix and leaves unreported codes empty, so the dataset keeps the provider's own vocabulary rather than a mapping this actor invented.

#### Does this cover agency and CRE CLO loans as well as conduit CMBS?

The provider's dataset spans the securitised universe, and this actor returns whatever your agreement covers for a given location. The deal type on every loan row tells you which programme a loan came from, so a mixed pull stays separable afterwards.

### Keyword map

CMBS data API, CRE loan data API, commercial real estate loan API, securitised loan data, CMBS loan lookup, special servicer data API, servicer watchlist API, CRE CLO data, loan maturity data API, DSCR data API, debt yield data, NOI data API, commercial property comps API, CRE sales comps API, loan comparables, property financial statements API, rent roll tenant data API, lease expiry data, CRE valuation data API, commercial mortgage data, conduit loan data, distressed CRE data, property ID lookup API, Plus Code property lookup

# Actor input Schema

## `mode` (type: `string`):

Resolve turns an address, Plus Code or Place ID into the provider's location ID, which every other mode keys off. Overview is the light record. Details adds the loan summary, the financial snapshot, both valuations and the top tenants. Search runs the same lookup across an address fragment and pages the matches. Comparables returns the properties scored closest on loan, sale, property or financial terms. Lists read a saved screen back out. Financials returns detailed statements period by period.

## `locationIds` (type: `array`):

One provider location ID per line. Used by overview, details, comparables and financials. Run resolve or search first if you only have addresses.

## `listIds` (type: `array`):

One saved list ID per line, as shown in your account. Used by the saved list and saved list locations modes.

## `plusCodes` (type: `array`):

One Plus Code per line, for resolve mode. A Plus Code identifies a location without an address, which is how you look up a building that has not been given one yet.

## `placeIds` (type: `array`):

One Google Place ID per line, for resolve mode. Useful when your upstream system already geocodes through Google and you want to join on that identifier rather than on text.

## `address` (type: `string`):

Street address to search for. The provider combines whatever address parts you give it into one query, so a partial address is a valid search rather than an error.

## `city` (type: `string`):

City name to search for, on its own or alongside the other address parts.

## `state` (type: `string`):

Two letter state abbreviation, for example NY or TX. Sent uppercase whatever you type.

## `zip` (type: `string`):

Five digit ZIP code to search for.

## `compType` (type: `string`):

What the comparables are compared on. Loan compares debt terms, sale compares transactions, property compares physical characteristics and financial compares trading performance. Leave empty and the provider returns both sale and loan comparables.

## `financialsFrom` (type: `string`):

Earliest financial as-of date to consider, as YYYY-MM-DD. Applies to financial comparables only.

## `financialsTo` (type: `string`):

Latest financial as-of date to consider, as YYYY-MM-DD. Applies to financial comparables only.

## `propertyType` (type: `string`):

Required for financials mode, and ignored everywhere else. The category codes inside a statement differ by property type, so one request covers one type: split a mixed portfolio into one run per type.

## `sourceCode` (type: `integer`):

Optional source filter for detailed financials, when your agreement gives you more than one source and you want to pin the run to one of them.

## `includeTenants` (type: `boolean`):

Emit the top tenants of each property as their own rows, with size, share of area and lease expiry. Turn this off to collect property rows only.

## `pageSize` (type: `integer`):

How many records to ask for per search request, up to 100. Lower it if your plan throttles large responses.

## `startAt` (type: `integer`):

Skip this many search results before collecting. Use it to resume a large search where a previous run stopped.

## `maxResults` (type: `integer`):

Stop after this many rows. Counts every row: locations, tenants, comparables, list entries and financial statements alike.

## `requestsPerMinute` (type: `integer`):

How fast to call the provider. Lower this if your plan's rate limit is being hit.

## `apiKey` (type: `string`):

Your own API key, sent in the x-api-key header. Either this or a client ID and secret is required. Stored encrypted and never written into the dataset. Can also be supplied as the DATA\_API\_KEY environment secret.

## `clientId` (type: `string`):

Your own OAuth2 client ID, used with the client secret to fetch an access token. Only needed when you are not using an API key.

## `clientSecret` (type: `string`):

Your own OAuth2 client secret. Exchanged for a token once per run and cached until it expires, rather than on every request.

## `baseUrl` (type: `string`):

Overrides the host the API is called on. Leave empty unless the provider has given you a different one.

## `tokenUrl` (type: `string`):

Overrides the identity server the client ID and secret are exchanged at. Leave empty unless the provider has given you a different one.

## Actor input object example

```json
{
  "mode": "searchLocations",
  "compType": "",
  "propertyType": "",
  "includeTenants": true,
  "pageSize": 25,
  "startAt": 0,
  "maxResults": 100,
  "requestsPerMinute": 60
}
```

# Actor output Schema

## `records` (type: `string`):

One row per record, alongside the identifier or search that produced it.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/cmbs-loan-data-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/cmbs-loan-data-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call nabeelbaghoor/cmbs-loan-data-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/cmbs-loan-data-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dCCxwmagJKbk0C3vS/builds/mHweToIfWbgxlEZbo/openapi.json
