Ford Public API Sources
Ford exposes its data through one public cxservices API used by the ai_ford
pipeline. No authentication credentials are required.
API Domain
| Domain | application-id | Used for |
|---|---|---|
www.ford.com/cxservices/ | 07152898-698b-456e-be56-d3d83011d0a6 | All endpoints — model catalog, dealer locator, inventory search, Build & Price configurator |
All requests send application-id, x-requested-with: XMLHttpRequest, and
Accept: application/json (see ai_ford.sources.constants).
The same cxservices API is also reachable at shop.ford.com. Responses are
byte-identical across both hosts, and the www.ford.com application-id is
authorized for every endpoint (the shop.ford.com app-id returns 401 for
inventory search). The pipeline therefore standardizes on www.ford.com with a
single application-id; the previous shop.ford.com split has been removed.
Raw dependency graph
The raw tier is fully data-driven — no model names or years are hardcoded. The model catalog is fetched live and drives every downstream fetch:
| Node | Raw asset | Upstream | Endpoint |
|---|---|---|---|
| a | ford/us/raw/model_slices_models | — | products/ModelSlices.json?make=Ford |
| b | ford/us/raw/model_slices_model_trims | a | products/ModelSlices.json (per model/year) |
| c | ford/us/raw/dealer_locator_dealers | — | dealer/Dealers.json |
| d | ford/us/raw/inventory_search | a + c | inventory/Search.json |
| e | ford/us/raw/configurator_config | b | config/Details.json |
Model List (node a)
GET https://www.ford.com/cxservices/products/ModelSlices.json?make=Ford
Called with only make=Ford, ModelSlices.json returns the full catalog as
Response.Model — a list of every model-year, each with ModelName, Year,
VehicleLineCode, VehicleType, CatalogId, and model-level Pricing
(Low/High base MSRP, destination charge, plan pricing).
This is the authoritative, real-time enumerator of which models and years
exist (~130+ model-year records spanning roughly 2021–2027). It replaced the
previously hardcoded model and year lists; model_trims and inventory both
derive their (model, year) fan-out from this asset.
ModelName is the exact string the other endpoints expect, and it does not
always match the marketing name. For example F-150 model-years 2022+ are
"F-150 F-150", and the Mustang Mach-E is "Mache". Always pass the catalog
ModelName verbatim — never a guessed display name.
Model Trims (node b)
GET https://www.ford.com/cxservices/products/ModelSlices.json
?appContext=T1&planType=MSRP&modelSliceDefiners=modelTrimName
&make=Ford&model={ModelName}&year={Year}
Called per (model, year) from the Model List with
modelSliceDefiners=modelTrimName, the response carries the model-level fields
above plus Response.Model.ModelSlices.ModelSlice[] — one entry per trim:
| Field | Description |
|---|---|
id | Trim id (e.g. F25-BASE) — matches the inventory Trim.ID |
name | Human-readable trim name (e.g. Base, Big Bend®) |
ConfigToken | Base configuration token for the trim — consumed by the configurator (node e) |
Pricing | Per-trim pricing |
This source produces the trim-grain models entity (enriched with the
model-level VehicleLineCode / VehicleType / CatalogId) and supplies the
ConfigTokens that drive the feature catalog.
Dealer Locator (node c)
GET https://www.ford.com/cxservices/dealer/Dealers.json
| Param | Description |
|---|---|
make | Always Ford |
state | US state code — returns all dealers in that state |
maxDealers | Maximum dealers to return (API rejects values >500) |
Response: Response.Dealer[] — dealer objects with PACode, Name,
Address (nested Street1, City, State, PostalCode, Country),
Latitude, Longitude, Phone, Fax, dealerType, FordCertified,
TimeZone.
Coverage strategy: one request per US state via the state param. States
are non-overlapping, so no deduplication is needed; the largest state stays well
under the 500-dealer cap.
Inventory Search (node d)
GET https://www.ford.com/cxservices/inventory/Search.json
| Param | Description |
|---|---|
make | Always Ford |
model | Catalog ModelName (from the Model List asset) |
year | Model year (from the Model List asset) |
dealerPACode | Dealer PA code (from the dealer locator asset) — required |
postalCode | Required by validation but ignored when dealerPACode is present |
limit | Results per page. Max 100 — the API rejects larger values with HTTP 400 |
offset | Zero-based offset into the result set |
There is no Radius parameter, and PageSize / PageNumber are not real
params (earlier code used them and silently got a single unpaginated page).
Pagination. offset must stay within [0, Total], where Total comes from
Response.VehicleSearch.Counts.Total:
offset == Total→ HTTP 200 with an empty result.offset > Total→ HTTP 400.
So paging is driven by the count, not by detecting a short page: fetch offset=0,
read Counts.Total, then advance offset by limit only while offset < Total.
This never issues a request at or beyond Total.
Response shape varies with result count — _extract_vehicle_list normalizes all
three:
| Result count | Response.VehicleSearch.Vehicles |
|---|---|
| many | {"Vehicle": [ {...}, {...} ]} (list) |
| one | {"Vehicle": {...}} (single object, not a list) |
| zero | "" (an empty string, not an object) |
Each vehicle contains:
- Model:
Model.ModelName,Model.Year,Model.Make - Trim:
Trim.ID(e.g.501A),Trim.Name(e.g.XLT) - Pricing:
Pricing.MSRP,Pricing.Invoice - Specs:
NormalizeTransmission,NormalizeDrive,NormalizeFuel,NormalizeCylinder,NormalizeDisplacement,NormalizeDoorType,NormalizeExtPaint,NormalizeTrimColor - Stock:
Vin,StockNumber,DealerPACode,DOL(days on lot),DLRMileage,VehicleStage,DemoFlag - Media:
ImageToken,WindowStickerURL - Features:
ConfigToken— an encoded string of the vehicle's installed option codes (codes after theConfig[|Make|Model|Year|Trim|prefix;~-prefixed codes are excluded options). Parsed per vehicle intofeature_refs(see below).
Per-vehicle features. Each inventory row carries feature_refs — a list of
{feature_code} structs parsed from its ConfigToken (installed codes only). The
consolidated tier resolves these one-to-many against the features entity via a
list-mode foreign key (on: [trim_id, model_year, feature_refs.feature_code]),
collecting the matched feature_ids into a feature_ids list on the inventory row.
This is how each vehicle's configuration is captured. (Caveat: for relabeled
nameplate-variant VINs the ConfigToken is the base config's, per the pollution
note above — so a hybrid's codes may reflect the gas configuration.)
Coverage strategy: the search requires a (dealer, model, year) filter — there
is no bulk or radius-only variant. make/model/year accept comma-separated
values forming an equal-length positional matrix, so the component sends one
matrix call per dealer covering every (model, year) from the Model List, paged
by Counts.Total. A 136-tuple matrix returns a dealer's full inventory with no cap.
Dealer PA codes and the (model, year) universe are both discovered at
materialization time. This collapses what would be ~136 per-model-year calls per
dealer into one paginated call.
Upstream dependency: the inventory raw component declares
upstream_dep: [ford/us/raw/model_slices_models, ford/us/raw/dealer_locator_dealers]
and receives both as a dict via fetch_with_upstreams().
model_name is unreliable; identity is trim_idThe model filter does not do exact matching for same-nameplate powertrain
families. Validated against live data:
- Variant relabeling. When the matrix includes a nameplate family (Escape /
Escape Hybrid / Escape Plugin Hybrid — one vehicle line, shared trim ids), the
API relabels the variants onto the base name (
Escape Hybrid→Escape) and rewrites theirConfigToken/ImageTokento the base config. This also happens for a single base-model query:model=Escapealone returns the hybrid VINs asEscape. - Near-lossless, except promos. The relabeled record keeps the correct
Trim.ID(e.g.F45-PLATINUM), drivetrain, and core pricing (BaseMSRP,Invoice, plan prices identical). The only differences aremodel_name, the tokens, and special-event promotional pricing — the base name picks up promo fields (SpecialEventBasePrice/SpecialEventMSRPDiscount) that the variant name does not surface.
Distinct models never cross-contaminate (F-150 F-150 vs F-150 Lightning,
Bronco vs Bronco Sport, Escape vs Expedition are all clean) — so batching every
model into one call is safe. Because of the variant relabeling, the models entity
is keyed on (trim_id, model_year), not model_name (see below), so a relabeled
VIN still joins correctly.
Models entity — two complementary sources
The consolidated models entity merges two trim-grain sources on
(trim_id, model_year) — not model_name, which the inventory endpoint
relabels for nameplate variants. trim_id (e.g. F45-PLATINUM, encoding the
vehicle line) is consistent across inventory, the catalog, and the configurator.
model_trims(catalog) — full trim universe,VehicleLineCode,VehicleType, base ConfigTokens, authoritative pricing.inventory— drivetrain attributes (transmission, displacement, fuel, cylinders) and on-lot trim signals that the catalog does not carry.
Each contributes columns the other lacks, so both are kept rather than one being
dropped. The cross-entity foreign keys (inventory → models, features → models) also
join on (trim_id, model_year).
Because (trim_id, model_year) collapses rows that differ on non-key columns, the
catalog (model_trims) models source uses a min-MSRP tiebreak: the same
trim id comes back under each nameplate variant query (Escape / Escape Hybrid /
Escape Plugin Hybrid), and hybrids/PHEVs price higher, so the lowest-MSRP row is
the ICE/base config — keeping model_name canonical (Escape rather than
Escape Hybrid). The inventory models source is left to the component's default
deterministic dedup (it already labels everything with the base name post
all-models relabel). The non-blocking discrepancies check still surfaces the
collapsed conflicts. A future enhancement will add a variant_names aggregation (the
list of nameplate names a trim is sold under) once the transformed component can
collect a column into a list while retaining its scalar canonical value.
ConfigToken format
Ford encodes a vehicle configuration as a ConfigToken string:
Config[|Ford|F-150|2026|501A.|67T.61B.86M.~99P.~44G.]
Segments after the metadata prefix (Config[|Make|Model|Year|Trim|) contain
option codes separated by .; codes prefixed with ~ are excluded. The base
ConfigToken for each trim comes from Model Trims (node b) and drives the
feature catalog fetch.
Configurator — feature catalog (node e)
GET https://www.ford.com/cxservices/config/Details.json
?appContext=T1&configToken={ConfigToken}&planType=MSRP
&returnAttributes=modelId;modelTrimName;modelTrimId
&partAttributes=part.*&sortPartPrice=Descending
The ConfigTokens are read from the upstream Model Trims asset
(ford/us/raw/model_slices_model_trims) — one base token per trim — so the feature
catalog is enumerated dynamically rather than from a hardcoded model list.
Response: Response.PartState.PartClass[] — part classes, each containing a
list of Part objects.
PartClass groups (typical)
| PartClass | Content |
|---|---|
| Packages | Equipment packages (Co-Pilot360, Tow Package, etc.) |
| Exterior / Interior Options | Wheels, bumpers, seats, trim, electronics |
| Accessories | Dealer-installed accessories |
| Engine / Transmissions / Drive | Powertrain choices |
| Paint / Wheel / Seat Type | Colors, wheels, seating |
Part fields
| Field | Description |
|---|---|
PartID | Option code (matches ConfigToken codes) |
Name | Human-readable feature name |
Description | Longer description |
Category | Category name |
Price | MSRP price delta (string, e.g. "7695.0") |
State | Included, Completed, Selectable, Excluded, UserSelected |
IsProductStandard | "true" if standard equipment |
MarketingDoNotDisplay | "true" for internal parts hidden from the UI |
Parts with MarketingDoNotDisplay=true are internal (powertrain assignments,
body codes, pep codes). The pipeline stores all parts and preserves the
marketing_do_not_display flag for downstream filtering.
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
FORD_INVENTORY_MAX_DEALERS | Cap dealer count for dev/testing | None (all) |
FORD_INVENTORY_MAX_PAGES | Cap pages per dealer query | None (all) |
FORD_INVENTORY_MAX_WORKERS | Inventory fan-out thread pool size | 10 |
FORD_CONFIGURATOR_MAX_CONFIGS | Cap ConfigTokens fetched by the configurator | None (all) |