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 built-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. This was 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 structure

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.

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.

The live asset issues one full-US sweep and stores every dealer in a single raw row, so the backfill sets bq_aggregate_partition to emit one row per partition too — omitting it produces one row per dealer, a structure no live partition has. Backfilled partitions yield 371 transformed dealers for 2026-06-25, matching live.

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 schedule.