Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.17.0] - 2026-07-08

### Added

- **LoopNet client** (`client.loopnet`) — commercial-real-estate (CoStar) listings, brokers, and reference data across loopnet.com/.ca/.co.uk/.fr/.es (US/CA/UK/FR/ES). Sub-clients: `search.search()` (for-lease / for-sale / auctions, all property types, filters, pagination), `listings.get()`, `brokers.get()`, `reference.markets()`, `reference.propertyTypes()`, with fully-typed maximal-coverage `LoopNet*`-prefixed types (`LoopNetSearchResponse`, `LoopNetListingDetail`, `LoopNetBrokerProfile`, `LoopNetListingCard`, etc.). LoopNet is behind Akamai Bot Manager (browser-farm-only) — served via the ScrapeBadger farm. (SCR-102)

## [0.15.6] - 2026-07-07

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const client = new ScrapeBadger();
| **eBay** | 12 endpoints across 18 markets — search, completed/sold listings, item detail, item reviews, seller profile/items/feedback, category browse, categories, autocomplete, markets | [eBay Guide](docs/ebay.md) |
| **YouTube** | 39 endpoints — search, autocomplete, video detail/related/comments/replies/transcript/captions/streams/live chat/batch, channel detail/videos/shorts/streams/playlists/community/about/subscriber count/search/resolve, playlist detail/items, mixes, trending/shorts, hashtags, home, shorts detail/by sound, community posts/comments, music search, oEmbed, categories/languages/regions/markets | [YouTube Guide](docs/youtube.md) |
| **Immobiliare** | 8 endpoints across 4 markets (it, es, gr, lu) — autocomplete, search, listing detail, agency profile/listings, price stats, markets, reference | [Immobiliare Guide](docs/immobiliare.md) |
| **LoopNet** | 5 endpoints across loopnet.com/.ca/.co.uk/.fr/.es — commercial-real-estate search (for-lease/for-sale/auctions), listing detail, broker profile, markets, property types | [LoopNet Guide](docs/loopnet.md) |

## Error Handling

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scrapebadger",
"version": "0.16.0",
"version": "0.17.0",
"description": "Official Node.js SDK for ScrapeBadger - Async web scraping APIs for Twitter, Google, Vinted, Reddit, and more",
"type": "module",
"main": "./dist/index.js",
Expand Down
5 changes: 5 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { RealtorClient } from "./realtor/client.js";
import { LeboncoinClient } from "./leboncoin/client.js";
import { ZillowClient } from "./zillow/client.js";
import { ImmobiliareClient } from "./immobiliare/client.js";
import { LoopNetClient } from "./loopnet/client.js";

/**
* ScrapeBadger API client.
Expand Down Expand Up @@ -98,6 +99,9 @@ export class ScrapeBadger {
/** Immobiliare scraper API client — 8 endpoints across 4 markets (it, es, gr, lu) */
readonly immobiliare: ImmobiliareClient;

/** LoopNet scraper API client — commercial real estate across US/CA/UK/FR/ES (search, listing, broker, markets, property types) */
readonly loopnet: LoopNetClient;

/**
* Create a new ScrapeBadger client.
*
Expand Down Expand Up @@ -151,5 +155,6 @@ export class ScrapeBadger {
this.leboncoin = new LeboncoinClient(this.baseClient);
this.zillow = new ZillowClient(this.baseClient);
this.immobiliare = new ImmobiliareClient(this.baseClient);
this.loopnet = new LoopNetClient(this.baseClient);
}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export * from "./zillow/index.js";
// Re-export Immobiliare module — ImmobiliareClient + Immobiliare*-prefixed types
export * from "./immobiliare/index.js";

// Re-export LoopNet module — LoopNetClient + LoopNet*-prefixed types
export * from "./loopnet/index.js";

// Re-export Leboncoin module — LeboncoinClient + Leboncoin*-prefixed sub-clients and types
export * from "./leboncoin/index.js";

Expand Down
54 changes: 54 additions & 0 deletions src/loopnet/brokers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* LoopNet Brokers API client.
*
* Provides a commercial broker's profile and their active listings.
*/

import type { BaseClient } from "../internal/client.js";
import type { BrokerResponse, LoopNetMarket } from "./types.js";

/** Options for the broker profile endpoint. */
export interface LoopNetBrokerParams {
/** Coverage market (default: "us") */
market?: LoopNetMarket;
}

/**
* Client for the LoopNet broker profile endpoint.
*
* @example
* ```typescript
* const client = new ScrapeBadger({ apiKey: "key" });
*
* const profile = await client.loopnet.brokers.get("jane-doe", "w7x2k9");
* console.log(profile.broker.name, profile.broker.listing_count);
* ```
*/
export class BrokersClient {
private readonly client: BaseClient;

constructor(client: BaseClient) {
this.client = client;
}

/**
* Get a LoopNet broker's profile and their active listings.
*
* Costs 8 credits.
*
* @param slug - The broker's profile URL slug.
* @param brokerId - The LoopNet broker id.
* @param params - Optional market selector.
* @returns Broker profile response with the broker and their listings.
* @throws NotFoundError - If the broker doesn't exist.
*/
async get(
slug: string,
brokerId: string,
params: LoopNetBrokerParams = {}
): Promise<BrokerResponse> {
return this.client.request<BrokerResponse>(`/v1/loopnet/brokers/${slug}/${brokerId}`, {
params: { market: params.market },
});
}
}
63 changes: 63 additions & 0 deletions src/loopnet/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* LoopNet API client.
*
* Provides access to all LoopNet API endpoints through specialized sub-clients.
*/

import type { BaseClient } from "../internal/client.js";
import { SearchClient } from "./search.js";
import { ListingsClient } from "./listings.js";
import { BrokersClient } from "./brokers.js";
import { ReferenceClient } from "./reference.js";

/**
* LoopNet API client with access to all LoopNet endpoints.
*
* Provides sub-clients for different resource types:
* - `search` - For-lease / for-sale / auction commercial-listing search
* - `listings` - Listing detail (by listing id)
* - `brokers` - Broker profile and their active listings
* - `reference` - Reference data (markets, property types)
*
* @example
* ```typescript
* const client = new ScrapeBadger({ apiKey: "key" });
*
* // Search listings
* const results = await client.loopnet.search.search({ location: "Houston, TX" });
*
* // Get listing detail
* const detail = await client.loopnet.listings.get("12345678");
*
* // Get a broker profile
* const broker = await client.loopnet.brokers.get("jane-doe", "w7x2k9");
*
* // Reference data
* const markets = await client.loopnet.reference.markets();
* ```
*/
export class LoopNetClient {
/** Client for for-lease / for-sale / auction commercial-listing search */
readonly search: SearchClient;

/** Client for listing detail (by listing id) */
readonly listings: ListingsClient;

/** Client for broker profile and their active listings */
readonly brokers: BrokersClient;

/** Client for reference data (markets, property types) */
readonly reference: ReferenceClient;

/**
* Create a new LoopNet client.
*
* @param client - The base HTTP client for making requests.
*/
constructor(client: BaseClient) {
this.search = new SearchClient(client);
this.listings = new ListingsClient(client);
this.brokers = new BrokersClient(client);
this.reference = new ReferenceClient(client);
}
}
43 changes: 43 additions & 0 deletions src/loopnet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* LoopNet API module.
*
* @module loopnet
*/

export { LoopNetClient } from "./client.js";
export { SearchClient as LoopNetSearchClient } from "./search.js";
export { ListingsClient as LoopNetListingsClient } from "./listings.js";
export { BrokersClient as LoopNetBrokersClient } from "./brokers.js";
export { ReferenceClient as LoopNetReferenceClient } from "./reference.js";

// Export request-param helper types
export type { LoopNetListingParams } from "./listings.js";
export type { LoopNetBrokerParams } from "./brokers.js";

// Export all response types
export type {
// Shared
Broker as LoopNetBroker,
Space as LoopNetSpace,
MarketInfo as LoopNetMarketInfo,
PropertyTypeInfo as LoopNetPropertyTypeInfo,
Pagination as LoopNetPagination,
// Search results
ListingCard as LoopNetListingCard,
// Listing detail
ListingDetail as LoopNetListingDetail,
// Broker profile
BrokerProfile as LoopNetBrokerProfile,
// Response envelopes
SearchResponse as LoopNetSearchResponse,
ListingResponse as LoopNetListingResponse,
BrokerResponse as LoopNetBrokerResponse,
MarketsResponse as LoopNetMarketsResponse,
PropertyTypesResponse as LoopNetPropertyTypesResponse,
// Param enums
LoopNetMarket,
LoopNetListingType,
LoopNetPriceType,
// Request params
LoopNetSearchParams,
} from "./types.js";
49 changes: 49 additions & 0 deletions src/loopnet/listings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* LoopNet Listings API client.
*
* Provides full listing detail lookup by listing id.
*/

import type { BaseClient } from "../internal/client.js";
import type { ListingResponse, LoopNetMarket } from "./types.js";

/** Options for the listing detail endpoint. */
export interface LoopNetListingParams {
/** Coverage market (default: "us") */
market?: LoopNetMarket;
}

/**
* Client for the LoopNet listing detail endpoint.
*
* @example
* ```typescript
* const client = new ScrapeBadger({ apiKey: "key" });
*
* const detail = await client.loopnet.listings.get("12345678");
* console.log(detail.listing.address, detail.listing.price_text);
* ```
*/
export class ListingsClient {
private readonly client: BaseClient;

constructor(client: BaseClient) {
this.client = client;
}

/**
* Get a single LoopNet listing's full detail by listing id.
*
* Costs 12 credits.
*
* @param listingId - The LoopNet listing id.
* @param params - Optional market selector.
* @returns Listing detail response.
* @throws NotFoundError - If the listing doesn't exist.
*/
async get(listingId: string, params: LoopNetListingParams = {}): Promise<ListingResponse> {
return this.client.request<ListingResponse>(`/v1/loopnet/listings/${listingId}`, {
params: { market: params.market },
});
}
}
49 changes: 49 additions & 0 deletions src/loopnet/reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* LoopNet Reference Data API client.
*
* Provides the static coverage-market and property-type lists.
*/

import type { BaseClient } from "../internal/client.js";
import type { MarketsResponse, PropertyTypesResponse } from "./types.js";

/**
* Client for LoopNet reference endpoints (markets, property types).
*
* @example
* ```typescript
* const client = new ScrapeBadger({ apiKey: "key" });
*
* const markets = await client.loopnet.reference.markets();
* const types = await client.loopnet.reference.propertyTypes();
* ```
*/
export class ReferenceClient {
private readonly client: BaseClient;

constructor(client: BaseClient) {
this.client = client;
}

/**
* List LoopNet coverage markets (us, ca, uk, fr, es).
*
* Free — costs 0 credits.
*
* @returns Markets response with all supported coverage markets.
*/
async markets(): Promise<MarketsResponse> {
return this.client.request<MarketsResponse>("/v1/loopnet/markets");
}

/**
* List LoopNet property-type facets (Office, Retail, Industrial, …).
*
* Free — costs 0 credits.
*
* @returns Property-types response with all searchable property-type slugs.
*/
async propertyTypes(): Promise<PropertyTypesResponse> {
return this.client.request<PropertyTypesResponse>("/v1/loopnet/property-types");
}
}
57 changes: 57 additions & 0 deletions src/loopnet/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* LoopNet Search API client.
*
* Provides commercial-listing search for for-lease / for-sale / auction
* listings across 5 markets (us, ca, uk, fr, es).
*/

import type { BaseClient } from "../internal/client.js";
import type { LoopNetSearchParams, SearchResponse } from "./types.js";

/**
* Client for the LoopNet listing search endpoint.
*
* @example
* ```typescript
* const client = new ScrapeBadger({ apiKey: "key" });
*
* const results = await client.loopnet.search.search({ location: "Houston, TX" });
* for (const listing of results.results) {
* console.log(`${listing.position}. ${listing.address} — ${listing.price_text}`);
* }
* ```
*/
export class SearchClient {
private readonly client: BaseClient;

constructor(client: BaseClient) {
this.client = client;
}

/**
* Search LoopNet for for-lease / for-sale / auction commercial listings.
*
* Costs 10 credits.
*
* @param params - Search parameters including location, filters, and pagination.
* @returns Search results with listing cards and pagination metadata.
* @throws AuthenticationError - If the API key is invalid.
* @throws ValidationError - If the parameters are invalid.
*/
async search(params: LoopNetSearchParams): Promise<SearchResponse> {
return this.client.request<SearchResponse>("/v1/loopnet/search", {
params: {
location: params.location,
market: params.market,
listing_type: params.listing_type,
property_type: params.property_type,
page: params.page,
min_price: params.min_price,
max_price: params.max_price,
price_type: params.price_type,
min_size: params.min_size,
max_size: params.max_size,
},
});
}
}
Loading
Loading