Skip to main content

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

EntityEndpointMethodCoverage
Inventory/services/graphql/retailerinventory/v2POSTPer-dealer fan-out (national ≈ 642 dealers)
Dealers/services/dealers/distances/by/zipcodeGETFull US population (≈642) via large count
Models (carline)/services/vehicles/models/basicdataGETAll active carlines (17 model-years)
Trims / Features / Colors/services/vehicles/trims/limiteddetails/{year}/{modelCode}GETPer (year, model)
Incentives (bonus)/services/specialoffers/byDealerGETPer (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. Without origin+refererHTTP 403.
  • Body: {"page": 0, "dealerCode": "020250,020207,…", "sortBy": "asc", "itemsPerPage": 30}
    • dealerCodeREQUIRED. 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=1000 from a central zip (66101, Kansas City) returns 642 dealers, 642 unique dealer.id, 642/642 with non-zero lat/lon — full national population in one call. count=5 → 5, count=500 → 500 (so count is 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 (⚠️ 0 here — real freight is in inventory destinationFee), 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 are KEY, Engine/Chassis, Exterior, Interior, Amenities, Safety. optionality{Standard, Optional, None}. Each feature's model FK is the trim-level modelCode.
  • 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 AFieldExampleSource BFieldExampleStatus
inventoryitems[].dealerCode020250dealersdealer.id020250✓ verified (both = Liberty Subaru)
inventoryitems[].modelCodeIMPmodels/basicdatacodeIMP✓ verified
inventoryitems[].trimCodeTLDtrims/limiteddetailscodeTLD✓ verified (IMP Sport)
inventoryitems[].exteriorColor.codeK1Xtrims/limiteddetailsexteriorColors[].codeK1X✓ verified
trimsmodelCodeIMPmodels/basicdatacodeIMP✓ verified
incentivesmodelName / modelCodeOutback / TDGtrimsmodelName / codeOutback / 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 code within featureGroups[].code, scoped by trim modelCode.

Freshness evidence

  • Live page: https://www.subaru.com/build.html shows the 2026 lineup (Crosstrek, Forester, Outback, Uncharted, Solterra, Trailseeker, Ascent, Impreza, BRZ, WRX + Hybrid variants).
  • models/basicdata returns those same carlines for 2025 + 2026 with matching base MSRPs; trims/limiteddetails/2026/OBK returns 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 just user-agent + accept. No bot friction.
  • POST /services/graphql/retailerinventory/v2403 unless origin: https://www.subaru.com and referer: https://www.subaru.com/vehicle-info/local-inventory.html are sent. With them, plain httpx.post(json=…) returns 200. No TLS impersonation / browser_session required.
  • 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.

FixtureSource callNotes
retailer_inventory_sample.jsoninventory POST, dealerCode=0202503 vehicles (2 IMP, 1 CTK); image maps stripped
dealers_by_zipcode_sample.jsondealers zipcode=10001&count=253 dealers incl. 020250
models_basicdata_sample.jsonmodels/basicdataall 17 carline-years
trims_limiteddetails_sample.jsontrims /2026/IMP2 trims (TLD, TLG); features/colors truncated
special_offers_sample.jsonspecialoffers OBK/TDG1 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 at pagedListWrapper.pager.totalItems).
  • Fan-out enumerators: dealers dealer.id → inventory dealerCode (REQUIRED fan-out); models (year, code) → trims limiteddetails/{year}/{code}.
  • Body-shape endpoint: retailerinventory/v2 (POST, requires origin+referer).
  • FK identity keys: inventory vinNumber; dealers dealer.id; models code+year; trims code.
  • Fixtures: 5 (listed above).
  • data_quality entities: inventory (tsrp/internetPrice populated, msrp sentinel 0, destinationFee nullable), dealers (location.latitude/longitude non-zero).

Open items / notes for the contract & implement phases

  • Prove inventory national coverage strategy: enumerate all ~642 dealer codes (dealers count=1000 from a central zip) and page retailerinventory/v2 per dealer or dealer-batch (CSV dealerCode accepts many — the site sent 22). Confirm the CSV batch size ceiling.
  • Confirm freight handling: trims.destinationCharge/dealerInvoice are 0; use inventory destinationFee for per-vehicle freight (nullable — decide null policy).
  • Confirm inventory msrp is always 0 across pages (contract data_quality_probe); map price to tsrp (and internetPrice for sale price).