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 domainwww.<brand>.com/<brand>/shopping/api/.... The brand-domain proxy is what carries the registeredclient: T1_VSRpivot config; calls sent directly to<brand>.aecloud.iofail withAEC_DISC_CONFIG_NOT_FOUND. Dealers now use GM's brand-domainquantum-dealer-locatorBAC endpoint instead of the retired AEC-CPlocate-dealerszip 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:
| Brand | facets models | inventory total | dealer source | configurator chain |
|---|---|---|---|---|
| chevrolet | 24 | 275,843 | quantum by BAC | ✅ bootstrap→catalogue→trim→features→state |
| gmc | (verified) | — | quantum by BAC | ✅ |
| buick | 4 | 48,113 | quantum by BAC | (shared) |
| cadillac | 15 | 24,663 | quantum 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
yearvalues: 2027, 2026, 2025. Configurator/bootstrapyears: 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.code1TU58_1LT= LT), Chevrolet Bolt MY2027, GMC Canyon MY2026, Cadillac CT4 MY2026. The configurator/trimreturns the same trims with live MSRP (Trax LS from1TR58_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 carriesstockDetails.stockNumberandvariant.chromeStyleId; VIN is the stable primary. - Reported-total path:
data.count(nationwide total for the query). - Pagination: cursor-based —
data.pagination.nextPageToken→ next request'spagination.nextPageToken.sizehard-caps at 20 regardless of requested value (tested to 500). Page-2 has 0 VIN overlap with page-1 (cursor verified live). Anoffsetfield 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:
bacis the 6-digit Business Associate Code that inventory references;dealerCodeis the separate 5-digit franchise code retained asdealer_code. - Implemented row identity: one row per brand franchise, keyed by
bac + dealer_code + brand. The extractor filterspayload.dealers[]to the franchise whosemakeCodescontain 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) withdata.trimListfallback. Identity:trimCode(e.g.1TU58_1LT) — used asmodel_code; matches inventoryvariant.code. Trim MSRP attrims.<sid>.msrp.value. - Enumerator:
/cataloguecalled with the full bodySegment × year filter (both from/bootstrap) returns the complete model/year/bodyStyle set that drives/trimfan-out. - AEC "failed" responses: a 200 can carry
status: "failed"witherrorDetails.key.aec.dr.cp.configurator.unsupported.vehicleandunknown.handledare expected for models with no configurator support (e.g. SAVANA, SIERRA EV) and yield no data — suppress, don't abort.
Features — two sources
-
/standard-features(standard-equipment source). Records pathdata.trim.highlights[]. Feature code:code(a real RPO). Fields:header(subcategory),description(name),longCfd(description),packageOption(bool →standard = not packageOption).model_codecomes from thetrimCoderecorded 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.headerIdis a CMS section id shared across most of the response (91 of 108 items in the captured Yukon fixture are10750, and all "Highlights" items arenull), so keying features on it collapsed ~91% of them into a handful of rows. OnlyshortCFDtext 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. -
/configurator-state(consolidated priced-options source, carriesmodel_code). It uses the option hierarchydata.workflow[].subCategories[].families{}.options[]withcode(RPO feature_code),msrpDisplay.amount.{value,text},included[](package sub-codes),selected,mostPopular. The fetcher tries every nonemptyconfigurations[].ssfrom/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.conditionisNEWon 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. - Dealers —
quantum-dealer-locatoris the current source. The oldlocate-dealerszip 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 —
/trimvia the/catalogueenumerator is the single trim-level catalogue withtrimCode+ 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-stateat priority 1 for priced options (carriesmodel_code)./loadwas retired from raw/transformed collection because it has no trim-levelmodel_code. Local 2026-07-24 verification found/configurator-statecovered 100% of/loadshared option keys and all non-null name, price, and package fields.
Quality-critical fields (for data_quality_probe)
| Entity | Fields (dotted path) | Verified this pass |
|---|---|---|
| dealers | geolocation.latitude / .longitude, bac, dealerCode | non-zero (e.g. 41.09782 / -73.99745); BAC + franchise code populated |
| inventory | pricing.cash.msrp.value, id (VIN), dealer.bac, variant.code | MSRP 26385/28085/50695; VIN/BAC/code populated |
| models | data.trims.<sid>.msrp.value, trimCode | Bolt LT $28,995; GMC Canyon $40,995 |
| features | code (feature_code, RPO), model_code (=trimCode) | populated; 6 coded highlights per trim (e.g. UVB, URL, KA1) |
Cross-source identity / FK alignment (value-matched)
| FK | Referencing entity.field | Target entity.field | Example value | Verified |
|---|---|---|---|---|
| inventory→dealers | inventory dealer.bac / transformed dealer_code | dealers bac + brand | 130555, 112024 | ✅ same 6-digit BAC space; implemented FK is dealer_code>bac plus brand |
| inventory→models | inventory variant.code | models trimCode | 1TU58_1LT, 1TU58_1SA, 1TU58_2RS | ✅ live — inventory codes ⊂ trax trim codes |
| features→models | features model_code (req trimCode) | models trimCode | 1TR58_1LS | ✅ recorded from /trim at fetch time |
| features(state)→models | configurator-state model_code (req trimCode) | models trimCode | 1TU58_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:
| Fixture | Call | Records path |
|---|---|---|
vehicles_facets_sample.json | POST vehicles/facets (model enumerator) | data.model[].values |
inventory_search_sample.json | POST vehicles/search (model=trax) | data.hits |
quantum_dealers_sample.json | GET quantum-dealer-locator (BAC 330505) | payload.dealers |
configurator_trim_sample.json | POST /trim (trax) | data.trims |
standard_features_sample.json | POST /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/facets→data.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_VSRrequired). - 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.jsonabove. - 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.
/loademits nomodel_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_VSRconfig; onlyNEW.