diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c4c53..191eb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c3a9554..8791018 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 99cf471..6ea0b96 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/client.ts b/src/client.ts index b375086..93d6556 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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. @@ -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. * @@ -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); } } diff --git a/src/index.ts b/src/index.ts index e3bbfb4..367ead2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/loopnet/brokers.ts b/src/loopnet/brokers.ts new file mode 100644 index 0000000..5bbd792 --- /dev/null +++ b/src/loopnet/brokers.ts @@ -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 { + return this.client.request(`/v1/loopnet/brokers/${slug}/${brokerId}`, { + params: { market: params.market }, + }); + } +} diff --git a/src/loopnet/client.ts b/src/loopnet/client.ts new file mode 100644 index 0000000..2a9644d --- /dev/null +++ b/src/loopnet/client.ts @@ -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); + } +} diff --git a/src/loopnet/index.ts b/src/loopnet/index.ts new file mode 100644 index 0000000..5ac89bd --- /dev/null +++ b/src/loopnet/index.ts @@ -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"; diff --git a/src/loopnet/listings.ts b/src/loopnet/listings.ts new file mode 100644 index 0000000..9dff392 --- /dev/null +++ b/src/loopnet/listings.ts @@ -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 { + return this.client.request(`/v1/loopnet/listings/${listingId}`, { + params: { market: params.market }, + }); + } +} diff --git a/src/loopnet/reference.ts b/src/loopnet/reference.ts new file mode 100644 index 0000000..34fd970 --- /dev/null +++ b/src/loopnet/reference.ts @@ -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 { + return this.client.request("/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 { + return this.client.request("/v1/loopnet/property-types"); + } +} diff --git a/src/loopnet/search.ts b/src/loopnet/search.ts new file mode 100644 index 0000000..66ac03a --- /dev/null +++ b/src/loopnet/search.ts @@ -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 { + return this.client.request("/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, + }, + }); + } +} diff --git a/src/loopnet/types.ts b/src/loopnet/types.ts new file mode 100644 index 0000000..4eb6334 --- /dev/null +++ b/src/loopnet/types.ts @@ -0,0 +1,315 @@ +/** + * TypeScript types for LoopNet API responses. + * + * These interfaces mirror the backend `loopnet_scraper` response schema + * field-for-field. Optional / nullable backend fields are typed as + * `Type | null`; backend list fields default to `[]` and are typed as arrays. + * Every datetime field ships in BOTH `*_utc` (number) and `*_at` (string) form. + * + * LoopNet is multi-market (us/ca/uk/fr/es); every search response carries + * `market` + `currency` so a caller can tell CAD from GBP. + */ + +// ============================================================================= +// Shared Types +// ============================================================================= + +/** A LoopNet broker/agent attributed to a listing or profile. */ +export interface Broker { + name: string | null; + company: string | null; + title: string | null; + phone: string | null; + email: string | null; + photo: string | null; + url: string | null; + broker_id: string | null; + city: string | null; + region: string | null; +} + +/** One leasable space / unit within a listing (lease listings). */ +export interface Space { + name: string | null; + space_use: string | null; + size_sqft: number | null; + size_text: string | null; + rent_text: string | null; + rent_per_sqft: number | null; + /** "/SF/YR", "/SF/MO", "/MO" */ + rent_period: string | null; + term: string | null; + condition: string | null; + available_date: string | null; + floor: string | null; +} + +/** A supported coverage market (for /markets). */ +export interface MarketInfo { + code: string; + domain: string; + country: string; + currency: string; + locale: string; + name: string; +} + +/** A LoopNet property-type facet (for /property-types). */ +export interface PropertyTypeInfo { + slug: string; + name: string; +} + +/** Page-number pagination (LoopNet returns ~25 cards per page, caps ~500). */ +export interface Pagination { + current_page: number; + per_page: number | null; + total_pages: number | null; + total_results: number | null; +} + +// ============================================================================= +// Search results +// ============================================================================= + +/** One LoopNet search result card (search / broker listings). */ +export interface ListingCard { + position: number; + listing_id: string | null; + property_id: string | null; + url: string | null; + // Taxonomy + /** for-lease | for-sale | auction */ + listing_type: string | null; + /** Office, Retail, Industrial … */ + property_type: string | null; + property_type_id: string | null; + space_use: string | null; + status: string | null; + status_id: string | null; + exposure_level: string | null; + is_auction: boolean | null; + // Content + title: string | null; + subtitle: string | null; + description: string | null; + /** LoopNet "N Star" building rating */ + building_rating: number | null; + year_built: number | null; + // Price + /** "$41.40 /SF/YR", "$2,500,000" */ + price_text: string | null; + price: number | null; + price_currency: string | null; + /** "/SF/YR", "/SF/MO", null for sale */ + price_period: string | null; + // Size + /** "901 - 15,746 SF" */ + size_text: string | null; + size_min_sqft: number | null; + size_max_sqft: number | null; + // Address + address: string | null; + city: string | null; + state: string | null; + zip: string | null; + county: string | null; + country: string | null; + market_id: string | null; + latitude: number | null; + longitude: number | null; + // Media / meta + thumbnail: string | null; + has_virtual_tour: boolean | null; + page_rank: number | null; + position_rank: number | null; + brokers: Broker[]; +} + +// ============================================================================= +// Listing detail +// ============================================================================= + +/** Full LoopNet listing detail (JSON-LD RealEstateListing + DOM facts). */ +export interface ListingDetail { + // Identity + listing_id: string | null; + property_id: string | null; + url: string | null; + market: string | null; + country: string | null; + /** for-lease | for-sale | auction */ + listing_type: string | null; + /** "For Lease" / "For Sale" */ + transaction_type: string | null; + // Content + name: string | null; + title: string | null; + subtitle: string | null; + description: string | null; + highlights: string[]; + // Price + price_text: string | null; + price: number | null; + price_currency: string | null; + price_period: string | null; + rental_rate_text: string | null; + cap_rate: number | null; + noi: string | null; + price_per_sqft: number | null; + // Building / property facts + property_type: string | null; + property_sub_type: string | null; + building_class: string | null; + building_size_sqft: number | null; + building_size_text: string | null; + rentable_building_area: string | null; + total_space_available: string | null; + total_space_available_sqft: number | null; + min_divisible: string | null; + max_contiguous: string | null; + typical_floor_size: string | null; + building_height: string | null; + ceiling_height: string | null; + year_built: number | null; + year_built_renovated: string | null; + building_rating: number | null; + lot_size_text: string | null; + lot_size_acres: number | null; + units: number | null; + stories: number | null; + percent_leased: string | null; + tenancy: string | null; + zoning: string | null; + parcel_id: string | null; + parking: string | null; + walk_score: number | null; + amenities: string[]; + // Address / geo + address: string | null; + city: string | null; + state: string | null; + zip: string | null; + county: string | null; + latitude: number | null; + longitude: number | null; + // Media + images: string[]; + photo_count: number | null; + videos: string[]; + documents: string[]; + has_virtual_tour: boolean | null; + // Spaces (lease) + brokers + spaces: Space[]; + brokers: Broker[]; + /** Every additionalProperty name/value pair LoopNet ships, verbatim */ + additional_facts: { name: string; value: string }[]; + // Timing + date_posted_utc: number | null; + date_posted_at: string | null; + date_updated_utc: number | null; + date_updated_at: string | null; + scraped_utc: number | null; + scraped_at: string | null; +} + +// ============================================================================= +// Broker profile +// ============================================================================= + +/** A LoopNet broker/professional profile with their listings. */ +export interface BrokerProfile { + broker_id: string | null; + name: string | null; + company: string | null; + title: string | null; + phone: string | null; + email: string | null; + photo: string | null; + url: string | null; + bio: string | null; + address: string | null; + city: string | null; + state: string | null; + license_number: string | null; + specialties: string[]; + listing_count: number | null; + listings: ListingCard[]; + scraped_utc: number | null; + scraped_at: string | null; +} + +// ============================================================================= +// Response Envelopes +// ============================================================================= + +/** Response from the /search endpoint. */ +export interface SearchResponse { + market: string; + country: string; + currency: string; + listing_type: string; + property_type: string | null; + location: string | null; + results: ListingCard[]; + pagination: Pagination; + scraped_utc: number | null; + scraped_at: string | null; +} + +/** Response from the /listings/{listing_id} endpoint. */ +export interface ListingResponse { + listing: ListingDetail; +} + +/** Response from the /brokers/{slug}/{broker_id} endpoint. */ +export interface BrokerResponse { + broker: BrokerProfile; +} + +/** Response from the /markets endpoint. */ +export interface MarketsResponse { + markets: MarketInfo[]; +} + +/** Response from the /property-types endpoint. */ +export interface PropertyTypesResponse { + property_types: PropertyTypeInfo[]; +} + +// ============================================================================= +// Request Parameter Types +// ============================================================================= + +/** Coverage market for LoopNet requests. */ +export type LoopNetMarket = "us" | "ca" | "uk" | "fr" | "es"; + +/** Listing-type filter for listing search. */ +export type LoopNetListingType = "for-lease" | "for-sale" | "auctions"; + +/** What min_price/max_price apply to. */ +export type LoopNetPriceType = "unit" | "sf" | "acre"; + +/** Parameters for the listing search endpoint. */ +export interface LoopNetSearchParams { + /** Location — "Houston, TX", ZIP, state code, or "usa" (required) */ + location: string; + /** Coverage market (default: "us") */ + market?: LoopNetMarket; + /** Listing type (default: "for-lease") */ + listing_type?: LoopNetListingType; + /** Property-type slug from /property-types (default: all) */ + property_type?: string; + /** Page number (1-20; LoopNet caps ~500 results) */ + page?: number; + /** Minimum price filter */ + min_price?: number; + /** Maximum price filter */ + max_price?: number; + /** What min/max price apply to */ + price_type?: LoopNetPriceType; + /** Minimum size (square feet) */ + min_size?: number; + /** Maximum size (square feet) */ + max_size?: number; +}