Skip to main content

BMW USA Dealer Locator Source

The dealer_locator source fetches the US-market dealer roster from the public bmwusa.com dealer locator servlet. It is specific to the us market of projects/ai_bmw and feeds the dealers entity alongside centernet (see CenterNet CRM Source).

Endpoint

GET https://www.bmwusa.com/bin/dealerLocatorServlet/?getdealerdetailsByRadius/57717/50000?includeSatelliteDealers=true

One request sweeps every US dealer — the servlet is queried once per partition with a hardcoded center-of-US zip code (57717) and a 50000-mile radius, no pagination. The response is a single ~1.9 MB JSON envelope containing ~446 dealer objects. There is no authentication.

The URL's doubled ? is sent to the wire verbatim (fetch_dealers_raw calls http_get_with_retry directly rather than the paginated/event helpers) — passing an explicit params dict would otherwise overwrite the baked-in query string.

Akamai / bot detection

The site sits behind Akamai, but the discriminator is header/client consistency, not the client itself: a plain httpx/requests GET with the default User-Agent succeeds, while the same request carrying a spoofed browser User-Agent hangs indefinitely (verified empirically — a browser UA on a non-browser TLS fingerprint trips bot detection). Do not add a browser User-Agent to this fetch; it sends only an explicit Accept: application/json, text/plain, */* header.

Response shape

dataContent.dealerDetails.dealerDetailsObjects: [
{
centerID: "...",
newVehicleSales: [ { ... } ],
certifiedPreowned: [ { ... } ],
service: [ { ... } ],
ccrc: [ { ... } ],
},
...
]

The same centerID can appear on multiple objects (e.g. distinct site classes), and each object carries up to four site arrays. Only newVehicleSales sites are extracted into BmwUsaDealerEntity — a physical dealer's certified-preowned, service, and CCRC sites are deliberately not extracted. They are preserved verbatim in the raw response for any future use.

Assets

TierAsset keyNotes
Rawbmw/us/raw/dealer_locator_dealerssource=dealer_locator, resource=dealers. One row per partition (the full envelope).
Transformedbmw/us/transformed/dealer_locator_dealersOne row per new-vehicle-sales site; key is (dealer_code, location_id).

dealer_code is the parent object's centerID (not present on the site object itself — merged in before validation); location_id is the site's own LocationID. A dealer's ag_code (BMW AG dealer code) can differ from its dealer_code for satellite sites.

Provenance

Migrated from the legacy S44-Automotive/bmw-dealer-data repo (a GCP Cloud Function writing to BigQuery bmw-prod-309117.inventory.dealer_locator_data_pit). The new ingestion queries the same public servlet directly rather than reading the legacy table.

Backfill

Partitions from 2025-07-16 onward materialize live. Partitions before that are backfilled from the legacy BQ table (dealer_locator_data_pit) via reshape functions in ai_bmw.backfill, with asset-backfill defs under defs/us/. The legacy table's history is weekly from July 2024 through December 2025, then daily — a partition without a legacy snapshot stays unmaterialized rather than being filled with stale or interpolated data.

Backfilled partitions lack fields the legacy table never stored — dpNumber, businessPartnerId, and programFlags are null (or absent from the raw envelope) on any pre-2025-07-16 partition.

Cadence

Daily at 10:00 UTC — matches the bmwusa private inventory cadence.