Subaru US — API Explore Summary
OEM: subaru Market: us Discovered: 2026-07-03
Base host: https://www.subaru.com (Adobe Experience Manager / Sling servlets under /services/)
All endpoints are same-origin JSON servlets on www.subaru.com. GET /services/*
endpoints are callable with only user-agent + accept headers (no bot friction). The
inventory POST endpoint additionally requires origin + referer (see Bot / 403 notes).
No Cloudflare/Akamai TLS impersonation is needed — plain httpx works.
Entities & selected endpoints
| Entity | Endpoint | Method | Coverage |
|---|---|---|---|
| Inventory | /services/graphql/retailerinventory/v2 | POST | Per-dealer fan-out (national ≈ 642 dealers) |
| Dealers | /services/dealers/distances/by/zipcode | GET | Full US population (≈642) via large count |
| Models (carline) | /services/vehicles/models/basicdata | GET | All active carlines (17 model-years) |
| Trims / Features / Colors | /services/vehicles/trims/limiteddetails/{year}/{modelCode} | GET | Per (year, model) |
| Incentives (bonus) | /services/specialoffers/byDealer | GET | Per (year, model, trim) |
Supporting/utility endpoints: /services/vehicles/modelyears (year enumerator 2017-2026),
/services/utility/geographicdata/?zipcode={zip} (zip → lat/lon/region/zone/district),
/services/dealers/distances/by/zipcode/maxDist?zipCode={zip}.
1. Inventory — POST /services/graphql/retailerinventory/v2
Despite the graphql path, the body is a plain JSON object, not a GraphQL query.
- Required headers:
content-type: application/json,origin: https://www.subaru.com,referer: https://www.subaru.com/vehicle-info/local-inventory.html. Withoutorigin+referer→ HTTP 403. - Body:
{"page": 0, "dealerCode": "020250,020207,…", "sortBy": "asc", "itemsPerPage": 30}dealerCode— REQUIRED. CSV of dealer codes (the site sends ~22 at once). Omitting it returns{"pagedListWrapper": null, "retailerInventorySummary": null}. This is the fan-out parameter; enumerate dealer codes from the Dealers endpoint (dealer.id).page— 0-based page index (verified: page 0 vs page 1 return disjoint VIN sets).itemsPerPage— page size (site uses 30).sortBy—"asc".
- records-path:
pagedListWrapper.items - id-field:
vinNumber(VIN). Secondary:stockNumber(dealer stock #). - total-path:
pagedListWrapper.pager.totalItems
Per-vehicle fields (pagedListWrapper.items[]):
vinNumber, dealerCode (FK→dealer), stockNumber, year, make, modelName,
modelCode (FK→model), trimCode (FK→trim), trimName, optionPackage{code,name},
optionPrice, options, bodyStyle, transmission, engine, cityMpg, highwayMpg,
msrp ⚠️ sentinel 0 — do not use, tsrp (real total suggested retail price),
internetPrice, destinationFee (⚠️ sometimes null), dealership, detailsUrl,
windowstickerUrl (/services/vehicles/windowsticker/{VIN}), inTransit,
exteriorColor{code,name,msrp,rgb,interiorColors[]}, interiorColor{code,name},
keyFeatures[]{code,name,description,optionality}.
Also returns retailerInventorySummary — cross-dealer aggregates (models, trims, features
with counts, dealers, colorCategories, min/maxPrice, lastUpdatedTimestamp). Useful as a
freshness signal but not the record source.
Pager also carries next: "/services/graphql/retailerinventory?page=1&itemsPerPage=30" (note:
drops the v2 segment — informational only; the working call is POST to …/v2).
Ready-to-run pagination probe:
uv run pagination_probe.py \
--url "https://www.subaru.com/services/graphql/retailerinventory/v2" \
--method POST \
--json-body '{"page":0,"dealerCode":"020250","sortBy":"asc","itemsPerPage":30}' \
--headers '{"origin":"https://www.subaru.com","referer":"https://www.subaru.com/vehicle-info/local-inventory.html","content-type":"application/json"}' \
--page-style page --page-param page --size-param itemsPerPage --page-size 30 \
--records-path "pagedListWrapper.items" --id-field "vinNumber" \
--total-path "pagedListWrapper.pager.totalItems"
(page is 0-based; dealerCode is a required baseline param — hold it constant while paging.)
Source selection: only one inventory endpoint found. National (no-dealerCode) is not
supported, so ingestion enumerates all dealers (Dealers endpoint, count large) and pages
retailerinventory/v2 per dealer or per dealer-batch. "only one source found."
Quality-critical fields: vinNumber (must be present/unique); price = tsrp or
internetPrice, NOT msrp (msrp is a constant 0); destinationFee nullable.
2. Dealers — GET /services/dealers/distances/by/zipcode
- Params:
zipcode(required),count(result size),type=Active. - Coverage:
count=1000from a central zip (66101, Kansas City) returns 642 dealers, 642 uniquedealer.id, 642/642 with non-zero lat/lon — full national population in one call.count=5→ 5,count=500→ 500 (socountis an honest limit, not capped below 642). - records-path:
$(root is a JSON array of{dealer, distance}) - id-field:
dealer.id(dealer code, e.g."020250") - total-path: none (bounded by
count).
dealer fields: id, name, address{street,street2,city,county,state,zipcode},
phoneNumber, servicePhoneNumber, faxNumber, siteUrl, types[] (e.g. Sales, Cpo,
Service, Active), location{latitude,longitude,region,zone,district,sneDealer}.
Wrapper also carries distance (miles from the query zip).
Source selection: one endpoint; national coverage + real coordinates → best on all axes. "only one source found."
Quality-critical fields: location.latitude/location.longitude (verified 642/642
non-zero, plausible US coords), dealer.id.
3. Models (carline enumerator) — GET /services/vehicles/models/basicdata
- No params. records-path:
$(root array, 17 records). id-field:code+year. - Fields:
year,code(carline code:OBK,CTK,FOR,UNC,SOL,TSK,ASC,IMP,BRZ,WRX),name,active,msrp(base MSRP),msrpformatted,hasHybridTrim,hybridMsrp,hasWildernessTrim, plus marketing image URLs. - This is the enumerator that drives the trims fan-out: each
(year, code)pair is a valid/trims/limiteddetails/{year}/{code}call. Current lineup: Crosstrek, Forester, Outback, Uncharted, Solterra, Trailseeker, Ascent, Impreza, BRZ, WRX (2025 + 2026).
Quality-critical fields: code, year, msrp (populated ✓).
4. Trims / Features / Colors — GET /services/vehicles/trims/limiteddetails/{year}/{modelCode}
Single richest configurator endpoint — covers trims, features, and colors at once.
- Path params:
{year}(e.g.2026),{modelCode}(e.g.OBK,IMP) — enumerated from models/basicdata. - records-path:
$(root array of trims). id-field:code(trim code, scoped by model+year). total-path: none. - Per-trim fields:
code(trim code, e.g.TDD),name,msrp(populated ✓),msrpformatted,dealerInvoice(⚠️0),destinationCharge(⚠️0here — real freight is in inventorydestinationFee),baseTrim{code,name},engine,cylinders,horsePower,mpgCity,mpgHighway,transmissions[],modelCode(FK→model),modelName,variation,active,types/typeCodes(category), and the three sub-collections below. - Features:
featureGroups[]{code,name,features[]{code,name,optionality,value}}— groups areKEY, Engine/Chassis, Exterior, Interior, Amenities, Safety.optionality∈{Standard, Optional, None}. Each feature's model FK is the trim-levelmodelCode. - Colors:
exteriorColors[]{code,name,rgb,msrp (color premium),default,interiorColors[]}. - Packages:
optionPackageStarlink{}(keyed by option code).
Source selection: one configurator source, exposes trim MSRP ✓, exterior color premiums
(exteriorColors[].msrp) ✓, per-trim feature availability ✓. Gap: freight/destination is
0 in this endpoint — real per-vehicle freight comes from inventory destinationFee. There is a
sibling …/trims/details/…? (untested — limiteddetails is what the live Build & Price page
calls; treat as the fresh source).
Quality-critical fields: trim code, msrp; feature code + modelCode FK;
exteriorColors[].msrp.
5. Incentives (bonus) — GET /services/specialoffers/byDealer?type={type}&year={y}&model={code}&trim={trimCode}
- Params:
type(e.g.LowRate),year,model(carline code),trim(trim code). - records-path:
offers. Fields:offers[]{modelCode (=trim code), modelYear, optionCode, modelName, incentive[]{incentive_type, startDate, endDate, retailRate, cash, term, title, shortTitle, offerDetails, disclaimer_text, dueDate}}. - Not one of the four core entities; captured as a candidate incentives source. Fan-out is
(year, model, trim)from models + trims.
Cross-source field alignment (value-matched)
| Source A | Field | Example | Source B | Field | Example | Status |
|---|---|---|---|---|---|---|
| inventory | items[].dealerCode | 020250 | dealers | dealer.id | 020250 | ✓ verified (both = Liberty Subaru) |
| inventory | items[].modelCode | IMP | models/basicdata | code | IMP | ✓ verified |
| inventory | items[].trimCode | TLD | trims/limiteddetails | code | TLD | ✓ verified (IMP Sport) |
| inventory | items[].exteriorColor.code | K1X | trims/limiteddetails | exteriorColors[].code | K1X | ✓ verified |
| trims | modelCode | IMP | models/basicdata | code | IMP | ✓ verified |
| incentives | modelName / modelCode | Outback / TDG | trims | modelName / code | Outback / TDG | ✓ verified |
All FKs resolve by value. Inventory is the hub: vinNumber (PK), dealerCode→dealers,
modelCode→models, (modelCode,trimCode)→trims.
Identity keys (HARD-gate candidates)
- Inventory:
vinNumber(VIN). - Dealers:
dealer.id(dealer code). - Models:
code+year. - Trims:
code(trim code), scoped by(modelCode, year). - Features: feature
codewithinfeatureGroups[].code, scoped by trimmodelCode.
Freshness evidence
- Live page:
https://www.subaru.com/build.htmlshows the 2026 lineup (Crosstrek, Forester, Outback, Uncharted, Solterra, Trailseeker, Ascent, Impreza, BRZ, WRX + Hybrid variants). models/basicdatareturns those same carlines for 2025 + 2026 with matching base MSRPs;trims/limiteddetails/2026/OBKreturns 6 current 2026 Outback trims (Premium…Touring XT).- Inventory
retailerInventorySummary.lastUpdatedTimestamp="11 AM UTC, 07/03/26"(today). - Conclusion: the selected endpoints are fresh for the US market.
Sibling-brand sweep
Subaru of America is a single-brand OEM (subsidiary of Subaru Corporation). No sibling US
brands share this platform. Sweep = N/A; onboard subaru only.
Bot / 403 notes
GET /services/*(dealers, models, trims, modelyears, specialoffers, geographicdata) — work with justuser-agent+accept. No bot friction.POST /services/graphql/retailerinventory/v2— 403 unlessorigin: https://www.subaru.comandreferer: https://www.subaru.com/vehicle-info/local-inventory.htmlare sent. With them, plainhttpx.post(json=…)returns 200. No TLS impersonation /browser_sessionrequired.- Dynatrace (
rxVisitor/dtCookie) and Adobe cookies are set by the site but are not required for any endpoint above.
Fixtures (projects/ai_subaru/tests/data/us/)
All correlated: dealer 020250 (Liberty Subaru) → its inventory (IMP/CTK) → trims for IMP 2026.
| Fixture | Source call | Notes |
|---|---|---|
retailer_inventory_sample.json | inventory POST, dealerCode=020250 | 3 vehicles (2 IMP, 1 CTK); image maps stripped |
dealers_by_zipcode_sample.json | dealers zipcode=10001&count=25 | 3 dealers incl. 020250 |
models_basicdata_sample.json | models/basicdata | all 17 carline-years |
trims_limiteddetails_sample.json | trims /2026/IMP | 2 trims (TLD, TLG); features/colors truncated |
special_offers_sample.json | specialoffers OBK/TDG | 1 LowRate offer |
Fixtures were truncated (image/360Images blobs removed, lists cut to 2-4 diverse records) to
stay well under 50 kB while preserving real structure and cross-source keys.
Coverage manifest additions (for discovery-contract.gate.json)
- Paginated endpoint:
retailerinventory/v2(page/itemsPerPage, total atpagedListWrapper.pager.totalItems). - Fan-out enumerators: dealers
dealer.id→ inventorydealerCode(REQUIRED fan-out); models(year, code)→ trimslimiteddetails/{year}/{code}. - Body-shape endpoint:
retailerinventory/v2(POST, requiresorigin+referer). - FK identity keys: inventory
vinNumber; dealersdealer.id; modelscode+year; trimscode. - Fixtures: 5 (listed above).
- data_quality entities: inventory (
tsrp/internetPricepopulated,msrpsentinel0,destinationFeenullable), dealers (location.latitude/longitudenon-zero).
Open items / notes for the contract & implement phases
- Prove inventory national coverage strategy: enumerate all ~642 dealer codes (dealers
count=1000from a central zip) and pageretailerinventory/v2per dealer or dealer-batch (CSVdealerCodeaccepts many — the site sent 22). Confirm the CSV batch size ceiling. - Confirm freight handling:
trims.destinationCharge/dealerInvoiceare0; use inventorydestinationFeefor per-vehicle freight (nullable — decide null policy). - Confirm inventory
msrpis always0across pages (contractdata_quality_probe); map price totsrp(andinternetPricefor sale price).