Skip to main content

GM — public API discovery summary (us)

  • OEM / market: gm / us
  • Brands (shared-infra family): Chevrolet, Buick, GMC, Cadillac
  • Re-verified live: 2026-07-21 (gm-rediscovery pass)
  • Platform: Tekion AEC-CP (*.aecloud.io), reverse-proxied through each brand marketing domain www.<brand>.com/<brand>/shopping/api/.... The brand-domain proxy is what carries the registered client: T1_VSR pivot config; calls sent directly to <brand>.aecloud.io fail with AEC_DISC_CONFIG_NOT_FOUND. Dealers now use GM's brand-domain quantum-dealer-locator BAC endpoint instead of the retired AEC-CP locate-dealers zip endpoint.

Shared headers

All AEC-CP inventory/configurator calls send:

Accept: application/json
Content-Type: application/json
oemId: GM
programId: CHEVROLET | BUICK | GMC | CADILLAC
tenantId: 0
dealerId: 0
client: T1_VSR

Akamai bot protection is active. All calls succeed with curl_cffi browser-TLS impersonation (impersonate="chrome") plus a seed GET to https://www.<brand>.com to plant ak_bmsc/bm_sv cookies. Plain httpx is blocked. No 403s observed with impersonation on any endpoint during this pass.

Sibling sweep result

Shared infrastructure CONFIRMED across all four US brands. Identical host pattern, identical JSON structures, identical headers — only programId/make + the brand subdomain change. Every entity endpoint was re-verified live for each brand this pass:

Brandfacets modelsinventory totaldealer sourceconfigurator chain
chevrolet24275,843quantum by BAC✅ bootstrap→catalogue→trim→features→state
gmc(verified)quantum by BAC
buick448,113quantum by BAC(shared)
cadillac1524,663quantum by BAC(shared)

Each brand is captured as its own brand-tagged source (tags: {brand: <brand>}); the consolidated tier unions the four brand-scoped transformed assets (one source_group per brand). Every URL dict in the source modules is keyed by all four brands.

Freshness evidence (product entities)

  • Chevrolet facets year values: 2027, 2026, 2025. Configurator /bootstrap years: 2028, 2027, 2026, 2025, 2024. Cadillac facets show 2027 model year (LYRIQ, ESCALADE IQ, VISTIQ). These are the current/forward model years the live brand sites show today — source is fresh, not stale.
  • Concrete current examples returned by the API and cross-checked: Chevrolet Trax MY2026 (variant.code 1TU58_1LT = LT), Chevrolet Bolt MY2027, GMC Canyon MY2026, Cadillac CT4 MY2026. The configurator /trim returns the same trims with live MSRP (Trax LS from 1TR58_1LS; Bolt LT $28,995; GMC Canyon Elevation $40,995).

Per-entity endpoints

Inventory — POST vehicles/search (AEC-CP discovery API)

POST https://www.<brand>.com/<brand>/shopping/api/aec-cp-discovery-api/p/v1/vehicles/search
Body:
{
"filters": {"geo": {"zipCode": "<zip>", "radius": <miles>},
"model": {"values": ["<model>"]}}, # model filter optional
"sort": {"name": "distance", "order": "ASC"},
"paymentTypes": ["CASH"],
"pagination": {"size": 20, "nextPageToken": "<cursor>"} # token omitted on page 1
}
  • Records path: data.hits
  • Identity field(s): id (VIN). Also carries stockDetails.stockNumber and variant.chromeStyleId; VIN is the stable primary.
  • Reported-total path: data.count (nationwide total for the query).
  • Pagination: cursor-baseddata.pagination.nextPageToken → next request's pagination.nextPageToken. size hard-caps at 20 regardless of requested value (tested to 500). Page-2 has 0 VIN overlap with page-1 (cursor verified live). An offset field is silently ignored — do not use it.
  • Coverage: search is geo-scoped but the count plateaus at the national total once the radius covers the country. One wide-radius query per model from a central zip (US_GEOGRAPHIC_CENTER_ZIP, US_NATIONWIDE_RADIUS) returns everything; the API clamps oversized radii rather than erroring. Production fans out one cursor-chain per model (models from the facets enumerator) for parallelism, not for coverage.
  • Key response fields (flat on each hit): id(VIN), make, model, year, bodyStyle, vehicleType, driveType, fuelType, baseExteriorColor, mileage, variant.{code,name,chromeStyleId}, stockDetails.{stockNumber,condition}, dealer.{name,postalCode,bac}, pricing.cash.{msrp,netPrice}.value, status.value.

Dealers — GET quantum-dealer-locator (GM brand-domain API)

GET https://www.<brand>.com/bypass/pcf/quantum-dealer-locator/v1/dealerId/<BAC>
Header: clientapplicationid: quantum
  • Records path: payload.dealers[].
  • Enumerator: distinct BACs from each brand's inventory raw rows (data.hits[].dealer.bac). The quantum raw asset is downstream of the corresponding inventory raw asset.
  • Identity fields: bac is the 6-digit Business Associate Code that inventory references; dealerCode is the separate 5-digit franchise code retained as dealer_code.
  • Implemented row identity: one row per brand franchise, keyed by bac + dealer_code + brand. The extractor filters payload.dealers[] to the franchise whose makeCodes contain the brand make code.
  • Key fields: dealerName, dealerUrl, address.{addressLine1,cityName,countrySubdivisionCode,postalCodeFormatted,countryIso}, geolocation.{latitude,longitude}, generalContact.phone1, makeCodes.

Models / trims — configurator chain (AEC-CP configurator gateway)

Five-endpoint dependency chain, all through www.<brand>.com/<brand>/shopping/api/aec-cp-configurator-gateway/p/v1:

GET  /bootstrap            → data.bodySegments[].value (quickFilter), data.years[].value (yearFilter)
POST /catalogue body {make, quickFilter[], yearFilter[]}
→ data.catalogue[].models[].years[] = (year, model, bodyStyle) combos
POST /trim body {year, make, model, bodyStyle, zipCode}
→ data.trims{<styleId>: {trimCode, name, msrp, configurations[].ss, ...}}
(some responses use data.trimList{<driveType>: [...]})
POST /standard-features body {year, make, model, bodyStyle, styleId} → data.trim.highlights[]
POST /configurator-state body {serializedState, style, zipCode} → data.workflow[] (priced, per-trim)
  • Models records path: data.trims (dict, keyed by styleId) with data.trimList fallback. Identity: trimCode (e.g. 1TU58_1LT) — used as model_code; matches inventory variant.code. Trim MSRP at trims.<sid>.msrp.value.
  • Enumerator: /catalogue called with the full bodySegment × year filter (both from /bootstrap) returns the complete model/year/bodyStyle set that drives /trim fan-out.
  • AEC "failed" responses: a 200 can carry status: "failed" with errorDetails.key. aec.dr.cp.configurator.unsupported.vehicle and unknown.handled are expected for models with no configurator support (e.g. SAVANA, SIERRA EV) and yield no data — suppress, don't abort.

Features — two sources

  1. /standard-features (standard-equipment source). Records path data.trim.highlights[]. Feature code: code (a real RPO). Fields: header (subcategory), description (name), longCfd (description), packageOption (bool → standard = not packageOption). model_code comes from the trimCode recorded in the request params (carries the FK to models).

    The much larger data.categories[].items[] list is deliberately not extracted. Those items carry no code of any kind. headerId is a CMS section id shared across most of the response (91 of 108 items in the captured Yukon fixture are 10750, and all "Highlights" items are null), so keying features on it collapsed ~91% of them into a handful of rows. Only shortCFD text distinguishes those items, and we do not synthesize identifiers. The full response is still retained in raw if GM ever exposes a per-item code.

  2. /configurator-state (consolidated priced-options source, carries model_code). It uses the option hierarchy data.workflow[].subCategories[].families{}.options[] with code (RPO feature_code), msrpDisplay.amount.{value,text}, included[] (package sub-codes), selected, mostPopular. The fetcher tries every nonempty configurations[].ss from /trim; GMC Sierra HD DRW trims reject the first gas state but answer with later diesel states, so only trims with no successful state are treated as raw failures.


Source-selection notes

  • Inventory — only one public source (vehicles/search). stockDetails.condition is NEW on the brand marketing sites; used/CPO is not exposed through this Tier-1 config. Winner by default; single call-family, full national coverage, MSRP populated.
  • Dealersquantum-dealer-locator is the current source. The old locate-dealers zip sweep used at onboarding was deleted after verification showed it captured only ~60% of dealers-with-inventory. Quantum resolves BACs from inventory directly and carries richer address/franchise fields. See Dealer Source.
  • Models/trim via the /catalogue enumerator is the single trim-level catalogue with trimCode + MSRP. Winner; no runner-up.
  • Features — two candidates captured intentionally: /standard-features (best standard-equipment coverage, clean per-trim FK) at priority 1 for standard equipment; /configurator-state at priority 1 for priced options (carries model_code). /load was retired from raw/transformed collection because it has no trim-level model_code. Local 2026-07-24 verification found /configurator-state covered 100% of /load shared option keys and all non-null name, price, and package fields.

Quality-critical fields (for data_quality_probe)

EntityFields (dotted path)Verified this pass
dealersgeolocation.latitude / .longitude, bac, dealerCodenon-zero (e.g. 41.09782 / -73.99745); BAC + franchise code populated
inventorypricing.cash.msrp.value, id (VIN), dealer.bac, variant.codeMSRP 26385/28085/50695; VIN/BAC/code populated
modelsdata.trims.<sid>.msrp.value, trimCodeBolt LT $28,995; GMC Canyon $40,995
featurescode (feature_code, RPO), model_code (=trimCode)populated; 6 coded highlights per trim (e.g. UVB, URL, KA1)

Cross-source identity / FK alignment (value-matched)

FKReferencing entity.fieldTarget entity.fieldExample valueVerified
inventory→dealersinventory dealer.bac / transformed dealer_codedealers bac + brand130555, 112024✅ same 6-digit BAC space; implemented FK is dealer_code>bac plus brand
inventory→modelsinventory variant.codemodels trimCode1TU58_1LT, 1TU58_1SA, 1TU58_2RS✅ live — inventory codes ⊂ trax trim codes
features→modelsfeatures model_code (req trimCode)models trimCode1TR58_1LS✅ recorded from /trim at fetch time
features(state)→modelsconfigurator-state model_code (req trimCode)models trimCode1TU58_1LT

Join keys: inventory→dealers joins transformed inventory dealer_code to dealers bac, scoped by brand. inventory→models and features→models join on [model_code, model_year].

Some current inventory rows carry special/fleet trim codes that do not appear in the public configurator's /trim catalogue for the same model year (for example commercial, work-truck, or fleet package variants). These remain visible as inventory model-FK misses rather than being inferred from nearby public trims.

Fixtures (packages/ai_public_api/tests/data/gm/us/)

Captured live 2026-07-21, Chevrolet, correlated (Trax MY2026 model, one dealer set), truncated, registered in tests/conftest.py:

FixtureCallRecords path
vehicles_facets_sample.jsonPOST vehicles/facets (model enumerator)data.model[].values
inventory_search_sample.jsonPOST vehicles/search (model=trax)data.hits
quantum_dealers_sample.jsonGET quantum-dealer-locator (BAC 330505)payload.dealers
configurator_trim_sample.jsonPOST /trim (trax)data.trims
standard_features_sample.jsonPOST /standard-features (trax)data.trim.highlights[]

(A pre-existing flat fixture set under tests/data/*.json feeds the current unit tests and is left in place; these us/ fixtures are the market-bucketed rediscovery capture.)

Ready-to-run pagination probe (inventory)

uv run pagination_probe.py \
--url "https://www.chevrolet.com/chevrolet/shopping/api/aec-cp-discovery-api/p/v1/vehicles/search" \
--method POST \
--headers '{"oemId":"GM","programId":"CHEVROLET","tenantId":"0","dealerId":"0","client":"T1_VSR","Content-Type":"application/json","Accept":"application/json"}' \
--body '{"filters":{"geo":{"zipCode":"66952","radius":5000}},"sort":{"name":"distance","order":"ASC"},"paymentTypes":["CASH"],"pagination":{"size":20}}' \
--page-style cursor \
--cursor-response-path "data.pagination.nextPageToken" \
--cursor-request-path "pagination.nextPageToken" \
--size-param "pagination.size" --page-size 20 \
--records-path "data.hits" --id-field "id" \
--total-path "data.count"

Impersonation is required — the probe harness must use the browser-TLS session with a seed GET to https://www.chevrolet.com, or Akamai returns a non-JSON block page.

Coverage-manifest additions (for discovery-contract.gate.json)

  • Paginated endpoint: pagination:inventory (cursor, data.pagination.nextPageToken).
  • Fan-out enumerators: enumerator:model (vehicles/facetsdata.model[].values); enumerator:year + enumerator:bodyStyle (/bootstrap → catalogue combos); enumerator:dealer (distinct inventory BACs → quantum-dealer-locator by BAC).
  • Body-shape / bot-block: body_shape:inventory, body_shape:features (Akamai + client:T1_VSR required).
  • FK identity keys: fk:inventory→dealers (dealer.bac / transformed dealer_code ↔ dealers.bac + brand), fk:inventory→models (variant.code↔trimCode), fk:features→models (trimCode).
  • Fixtures: the five tests/data/us/*_sample.json above.
  • Data-quality entities: data_quality:inventory, data_quality:dealers, data_quality:models, data_quality:features.
  • Freshness sidecars: freshness:models, freshness:features (years 2025–2028 live).

Notes / open items

  • All entities have a live, healthy endpoint; nothing blocked.
  • /load emits no model_code (keyed at model/bodyStyle) — FK-weak and covered by /configurator-state; retired from raw/transformed collection unless later fanned out per trim.
  • Used/CPO inventory is not exposed via the Tier-1 T1_VSR config; only NEW.