Documentation
Field references, integration guides, and product docs. Pick a product to dive in.
Vehicle Listings Data
A read-only API for live vehicle inventory — search, look up by id or VIN, and pull market stats. Get a key and pay per use.
https://luxe.hermesdata.io/v1_2/listingsOverview
Vehicle Listings is a read-only REST API for live vehicle inventory across the US & Canada. Every listing is tied to the dealership selling it. Search and filter the catalog, look up a vehicle by id or VIN, or pull market stats. v1.2 adds the dealer layer: every row names the website it came from and, where we can place it, the dealership entity behind it.
What's new in v1.2
Change the prefix, keep everything else. Every /v1/listings/* route is mirrored at /v1_2/listings/* with the same key, the same rate limits, the same paging, the same errors and the same prices. A client that changes nothing but the prefix gets the same data plus dealer identity on every row.
dealer_site_id on every row
A stable id for the website a listing came from, so you can group listings by rooftop without string-matching domains.
dealer_id where we know the dealership
The dealership that runs the site a listing came from - the same id the Dealers and Dealer Groups products use, so listings join to firmographics one-to-one.
entity_dealer_id for co-located stores
Stores sharing one address are one business. This is the roll-up key for a whole campus - and it is many-to-one, so never use it as a per-rooftop id.
Dealer-aware VIN lookups
One car listed on several sites returns one representative plus every other claim, instead of an arbitrary pick.
Resolve endpoints
Turn a site id into the dealership behind it, its name, and its sibling sites - one at a time or 500 per call.
v1 is not deprecated and has no scheduled removal date - pin whichever version you like. . One change is not a v1.2 feature and applies to both: photo_urls is returned in the dealer's own order, with photo_urls[0] as the lead photo.
This API · query
Self-serve, per call
- Search, filter, and look up listings in real time
- JSON over HTTPS, billed per request
Bulk file delivery
Enterprise plan
- Full-feed exports and recurring file deliveries
- Scope, schedule, and pricing set per arrangement
There is no bulk file export or download endpoint on the query API.
Create one API key in your portal. It's your bearer token for every request.
Search and filter, look up by id or VIN, or pull dealer inventory and market stats.
No subscription. Each request spends a little from one shared credit wallet.
Dealer identity
A listing is published on a website. A website belongs to a dealership, and a dealership often runs several websites. v1.2 hands you both levels: the website is always identified, and the dealership is identified whenever we can tie the two together.
dealer_site_ididentifies a website
Always presentIdentifies the dealer website a listing was published on. Derived from the listing's own domain, so it is there even for sites we hold no dealership record for. Treat it as an opaque, stable string.
dealer_ididentifies a dealership
Present when resolvedIdentifies the dealership that runs this exact website. A direct match, never a neighbour's id - so this is what you attribute a car to. Present only when a dealership record claims the site - ABSENT (not null) otherwise. Joins one-to-one to the Dealers and Dealer Groups products.
entity_dealer_ididentifies a business
Present when resolvedIdentifies the business a storefront belongs to. Dealerships at one physical address are one business even though each runs its own site, so this is MANY-TO-ONE: use it to roll a campus up, and never as a per-dealership key or your totals count a campus once per store on it.
resolutionidentifies how we linked them
Resolve responses onlyHow the BUSINESS was reached; dealer_id is a direct match either way. exact = the business's own site · redirect = joined through a redirect · cluster = this storefront was folded into a co-located sibling · unknown = no business (or identity not loaded).
Worked example: one dealership, two websites
Example Ford runs two sites for one physical rooftop, and a third site in the same market is a dealer we hold no record for. Three websites, three site ids, one dealership:
| Website | dealer_site_id | dealer_id | entity_dealer_id | resolution |
|---|---|---|---|---|
| exampleford.com | 4c1f9a2e7b83d05614af2c9e3d70b8a1 | d_10c4f2 | d_10c4f2 | exact |
| exampleford.ca | b73e0d41c85a9f2607d1e4b39c8f5a20 | absent | d_10c4f2 | redirect |
| examplekia.com | 9a4d17e6c0b23f8514ed70a9b62c4f38 | d_58e7b3 | d_10c4f2 | cluster |
| northsideauto.example | e05b6c37a91d4f28b3c7069e5a1d82f4 | absent | absent | unknown |
- Group by
dealer_site_idand you get four buckets - one per website. - Group by
dealer_idand you get one bucket per dealership. The old domain has no dealership of its own - it only redirects - so it drops out. Group byentity_dealer_idinstead and the first three collapse into one business, because the Kia store shares the Ford store's lot. The stranger has neither key, so fall back to its site id. - Resolving either of the first two names the other in its
sibling_site_ids(capped at 50). - Inventory by dealer id serves one storefront -
/dealers/id/d_10c4f2/inventory- while inventory by entity id serves the whole campus as one paged result set:/dealers/entity/d_10c4f2/inventory.
Code for a missing dealer_id
dealer_site_id is on every row. dealer_id is not: when a website doesn't resolve to a dealership we know, the key is absent from the object entirely - not null, not an empty string. The same is briefly true of every row in the window after a deploy, while the dealer-resolution map loads. Read it defensively (row.get("dealer_id"), row.dealer_id ?? null) and never treat its absence as an error. You can see it in the Quickstart response.
Coverage is not total: as of 2026-08-16, 100% of Ontario and 99% of Quebec rooftops sampled resolved to a dealership. A website we hold no dealer record for still gives you a usable dealer_site_id.
Stability: what's safe to store
A dealer_site_id is a stable, opaque 32-character string for one website. Persist it indefinitely - the same website always yields the same id. Read it off any row, or ask /dealers/resolve/…; don't try to derive or parse one.
Site identity follows the domain. If a dealer moves to a new domain, that is a new dealer_site_id. Continuity across a rename is dealer_id's job, which is why it is the right key for long-term storage.
Identity is per website, not per building. A single domain fronting several physical rooftops (per-path microsites on a shared host) is one site and one id - we cannot split it at this granularity. Use the listing's own city / postal code when you need to separate those rooftops.
While identity is warming up
The dealer-resolution map loads asynchronously after a deploy. v1.2 keeps serving throughout - nothing 503s and no shape changes - but the dealership half of identity is briefly missing. Build for this column and you are also built for websites we simply can't place:
| Feature | Normally | While warming up |
|---|---|---|
| dealer_site_id on rows | Present | Present - computed from the listing's own domain |
| dealer_id on rows | Present when resolved | Absent |
| entity_dealer_id on rows | Present when resolved | Absent |
| site_id search filter | Works | Works - it never needed identity |
| dealer_id search filter | Scopes to that dealership's own site | Empty result set (200, not an error) |
| entity_dealer_id search filter | Expands to every site of the business | Empty result set (200, not an error) |
| resolve endpoints | Full payloads | resolution: "unknown", netloc still echoed when we know the site |
| inventory by dealer_id | That dealership's own cars | Falls back to the single-site v1 behavior |
| inventory by entity_dealer_id | Union of every site of the business | 404 - there is no v1 shape to fall back to |
| VIN claim groups | Sites of one business merge into one claim | Every site is its own claim - nothing merges |
Joining listings to firmographics
dealer_id is the dealership that runs the website a listing came from - the same identifier the Dealers and Dealer Groups products key on, one-to-one. Where a rooftop exists in that catalogue, its listings and its firmographic record (legal name, phone lines, franchise mix, headcounts, martech) line up on one id, so you can go from "who is advertising this car" to "who do I call" without matching on names or domains. A listing itself never carries firmographics, and a website we have no dealer record for has no dealer_id to join on.
entity_dealer_id answers the other question - which business a storefront belongs to. Dealerships at one address are one business even though each runs its own site, so it is many-to-one: roll a campus up with it, and never use it as a per-dealership key or a per-rooftop total counts that campus once per store on it.
Authentication
Send your API key as a bearer token on every /v1_2/listings/* request. The key identifies you, resolves to your shared credit wallet, and carries its own rate limit (100 requests/second).
Authorization: Bearer hd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGenerate and manage your key from the API key page. Keep it secret - you only see it in full once. Auth fails closed: an empty or frozen wallet is refused. The liveness probe GET /health needs no key. One key covers both versions - nothing to re-issue or re-provision to move from /v1 to /v1_2.
Conventions
These rules hold across every endpoint.
All routes live under https://luxe.hermesdata.io/v1_2/listings. Requests and responses are JSON.
Only fields with a value appear in a record, so id is the one field you can always count on. Don't assume a key is present.
Every response carries an X-Request-Id. Send your own to have it echoed; quote it in support requests.
Units
The catalog spans the US and Canada, and values are passed through in the units the listing was published in - nothing is converted at ingest. Read province_state_code to know which you have:
| Field | US listing | Canadian listing |
|---|---|---|
| sale_price | USD | CAD |
| odometer | miles | kilometres |
| distance_km | Always kilometres, both countries. | |
The practical consequences: a cross-border price_min/price_max or odometer_min/odometer_max filter compares raw numbers across two units, and sort=price_asc orders USD and CAD in one list. Filter to one country (or convert on your side) when that matters. Aggregates from the stats endpoints inherit the same mix.
Response envelopes
You'll get one of three shapes, depending on the endpoint:
Lists
{ "data": [ … ], "meta": { … } }Search, dealer inventory, recents, and batch come wrapped in data + meta.
Point lookups
{ "id": "v_9f3a21", … }One vehicle by id or VIN comes back on its own - no wrapper. Resolve payloads are bare objects too.
Analytics
{ "regions": [ … ] }Stats endpoints each keep their own shape (comparables, stats, mds, popular…).
Two null conventions. Cards are sparse: a field with no value is omitted, so an unresolved dealer_id means the key isn't there at all. The claim entries inside a VIN lookup (co_listings / other_claimants) are the opposite - a fixed shape with explicit nulls, so you can index every field unconditionally.
Pagination
List routes take page (1-based) and page_size (1–1000, default 25), and report meta.total - the full match count across the catalog. A page_size over 1000 is clamped silently and echoed back at 1000, so read the echo rather than assuming your value was honoured.
A scan call bills one charge per started 100 rows returned: 1 and 100 rows both cost one charge, 250 rows cost three, 1000 rows cost ten. Same per-row price at any page_size, so a wide page saves round trips, not credits. A short page is billed on what arrived.
Numbered pages stop at max_offset (see /meta) - go past it and you get 400 offset_too_deep. To go deeper, page by cursor instead: every paged response carries meta.next_cursor, and sending it back as ?cursor=… resumes exactly where the last page stopped. There is no depth limit and no extra charge - the ten-thousandth page costs what the first one does. Keep the route, sort and filters identical across the walk (a mismatch is 400 invalid_cursor), don't send page and cursor together, and treat the token as opaque. Cursors are pinned to a catalog snapshot: if the nightly rebuild lands mid-walk you get 400 snapshot_expired naming the new version, rather than silently skipped or repeated rows.
import requests
params = {"make": "Ford", "page_size": 100, "sort": "price_asc"}
headers = {"Authorization": "Bearer hd_live_your_key_here"}
while True:
resp = requests.get("https://luxe.hermesdata.io/v1_2/listings/search", params=params, headers=headers)
resp.raise_for_status() # 400 snapshot_expired -> restart the walk
body = resp.json()
for listing in body["data"]:
print(listing["id"])
cursor = body["meta"].get("next_cursor")
if not cursor: # null on the last page
break
params["cursor"] = cursor # keep the filters and sort - the token only says where to resume
params.pop("page", None) # page and cursor are mutually exclusiveResponse headers
| Header | Sent on | Meaning |
|---|---|---|
| X-Request-Id | every response | Correlation id - echoed if you send one, else generated. Quote it in support requests. |
| Retry-After | 429 · 503 | Seconds to wait before retrying. |
| X-RateLimit-Limit | 429 | Your per-key sustained request budget (requests/second). |
| X-RateLimit-Remaining | 429 | Requests left in the current budget after this response. |
| X-RateLimit-Reset | 429 | Seconds until the budget refills to full. |
Quickstart
Grab your key from the API key page and make your first call against live data. It searches the catalog and returns matching listings in a { data, meta } wrapper:
GET /v1_2/listings/search?make=Toyota&model=RAV4&year_min=2023&page_size=3A few recent RAV4s. Every search works the same way: add your key, filter with query params, then read the results in data - each row carrying the website it came from and, where we know it, the dealership behind it.
curl "https://luxe.hermesdata.io/v1_2/listings/search?make=Toyota&model=RAV4&year_min=2023&page_size=3" \
-H "Authorization: Bearer hd_live_your_key_here"{
"data": [
{
"id": "v_77c1e0",
"vin": "2T3P1RFV8SC123456",
"make_name": "Toyota",
"model_name": "RAV4",
"trim_name": "XLE",
"year": 2025,
"sale_price": 34210,
"odometer": 8,
"province_state_code": "TX",
"city": "Austin",
"dealer_netloc": "exampletoyota.com",
"dealer_site_id": "7d24c0e918b6a53f42910cd7e6b38f5a",
"dealer_id": "d_44b19e",
"entity_dealer_id": "d_44b19e",
"new_vehicle": true,
"first_seen": "2026-07-24"
},
{
"id": "v_5ab902",
"make_name": "Toyota",
"model_name": "RAV4",
"trim_name": "Limited",
"year": 2024,
"sale_price": 31995,
"odometer": 12480,
"province_state_code": "TX",
"city": "Dallas",
"dealer_netloc": "northsideauto.example",
"dealer_site_id": "e05b6c37a91d4f28b3c7069e5a1d82f4",
"certified": true,
"first_seen": "2026-07-11"
}
],
"meta": { "page": 1, "page_size": 3, "total": 862, "next_cursor": "eyJ2IjoiMDgzMTk2…" }
}
// Two rows, two states of dealer identity: the first resolved to a dealership
// entity, the second did not, so its dealer_id key is absent entirely. Both
// carry dealer_site_id - that one is always there.There is no separate sandbox - you call live data, metered per API call by request type. A keyed lookup by id or VIN is the smallest charge, and paging search results runs about 10,000 listings per credit. From here, browse the full API reference or see every field in the data schema.
API reference
Every route, grouped. Each row expands to its full input contract - a typed parameter table - a runnable request in your language, and a real response body. Base URL https://luxe.hermesdata.io.
Discovery
Ask the catalog what it holds. Read these instead of hard-coding filter menus, sort options, limits, or prices.
Point lookups
Get one vehicle by id or VIN. These return the object on its own - no wrapper. A VIN lookup also tells you every dealer advertising that car.
Search & browse
Filter, page, and sort lists of listings - now scopable to one website or one whole dealership. These return the { data, meta } wrapper.
Dealer identity
Turn a website into the dealership behind it, and pull inventory by either. See Dealer identity above for the concepts.
Analytics
Market stats across the catalog, unchanged from v1. Each returns its own keyed shape, not the { data, meta } wrapper.
Taxonomy
Fill dropdowns and type-ahead boxes from the faceted fields (listed under Data schema). Unchanged from v1.
Service
Liveness probe. No key, no charge - for uptime checks. Not version-scoped.
Recipes
Four end-to-end jobs. Each one is complete - paste it, swap your key, and it works.
Group a market's listings by rooftop
dealer_site_id is a stable key per website, so bucket on it rather than normalizing domains yourself. Dealerships that run several sites collapse further on dealer_id.
import requests
from collections import defaultdict
BASE = "https://luxe.hermesdata.io/v1_2/listings"
H = {"Authorization": "Bearer hd_live_your_key_here"}
by_site, by_dealer = defaultdict(list), defaultdict(list)
params = {"state": "TX", "make": "Ford", "page_size": 100}
while True:
body = requests.get(f"{BASE}/search", params=params, headers=H).json()
for row in body["data"]:
by_site[row["dealer_site_id"]].append(row["id"])
dealer = row.get("dealer_id") # ABSENT when unresolved
if dealer:
by_dealer[dealer].append(row["id"])
cursor = body["meta"].get("next_cursor")
if not cursor:
break
params = {**params, "cursor": cursor}
print(len(by_site), "websites,", len(by_dealer), "known dealerships")Pull every listing a dealership has, across all its sites
Two ways in, same union: filter search by dealer_id when you want to combine it with other filters, or call the by-id inventory route when you just want the lot.
# Filterable: dealer_id ANDs with every other search param.
curl "https://luxe.hermesdata.io/v1_2/listings/search?dealer_id=d_10c4f2&year_min=2024&sort=price_desc" \
-H "Authorization: Bearer hd_live_your_key_here"
# Or the whole lot, unioned across every website the dealership runs.
curl "https://luxe.hermesdata.io/v1_2/listings/dealers/id/d_10c4f2/inventory?page_size=100" \
-H "Authorization: Bearer hd_live_your_key_here"
# Neither errors on an id we can't place: the filter returns an empty result
# set, the inventory route returns 404 dealer_not_found.Resolve a whole day's sites in one pass
Batch resolve costs one scan charge per request, whatever the id count - so chunk at the 500 cap rather than resolving row by row.
import requests
BASE = "https://luxe.hermesdata.io/v1_2/listings"
H = {"Authorization": "Bearer hd_live_your_key_here"}
# site_ids collected from a day of search / recents rows
site_ids = sorted({row["dealer_site_id"] for row in todays_rows})
resolved = {}
for i in range(0, len(site_ids), 500): # 500 is the per-call cap
chunk = site_ids[i:i + 500]
body = requests.post(f"{BASE}/dealers/resolve/batch",
json={"site_ids": chunk}, headers=H).json()
for payload in body["data"]: # same order as the input
resolved[payload["site_id"]] = payload
known = [p for p in resolved.values() if p["resolution"] != "unknown"]
print(f"{len(known)}/{len(site_ids)} sites tied to a dealership")Read a VIN's claims: who has it, and what they're asking
A VIN lookup returns one representative listing plus every other dealer advertising the same car, so you see every claim on it rather than one arbitrary pick.
import requests
VIN = "1FTFW1E84PFA12345"
car = requests.get(f"https://luxe.hermesdata.io/v1_2/listings/vehicles/{VIN}",
headers={"Authorization": "Bearer hd_live_your_key_here"}).json()
print("representative:", car["dealer_netloc"], car.get("sale_price"))
# Same dealership, its other websites.
for row in car["co_listings"]:
print(" also at", row["dealer_netloc"], row["sale_price"])
# Other dealerships claiming this VIN. dealer_id is explicitly null here when
# the site is unresolved - claim entries are fixed-shape, unlike sparse cards.
for row in car["other_claimants"]:
who = row["dealer_id"] or row["dealer_site_id"]
print(" claimed by", who, "at", row["sale_price"])Site ids in these examples (4c1f9a2e7b83d05614af2c9e3d70b8a1) are illustrative - real ones come off your own rows or from /dealers/resolve/….
Data schema
Every listing route returns a card - the fields below, grouped by topic. Only fields with a value appear, and id is always there.
Dealer identity · added by v1.2
On top of every card field below, v1.2 rows carry:
dealer_site_id· string · the website, 32 hex chars · always presentdealer_id· string · the dealership entity · absent when unresolved
Identity
id· string · always presentvin· stringmake_name· stringmodel_name· stringtrim_name· stringyear· integer
Specs & pricing
sale_price· integer · USD in the US, CAD in Canadaodometer· integer · miles in the US, km in Canadabody_style· stringfuel_name· stringdrive_train· stringtransmission_name· stringexterior_color· stringinterior_color· stringcertified· booleannew_vehicle· boolean
Location & lifecycle
province_state_code· stringcity· stringpostal_code· stringlatitude· numberlongitude· numberdealer_netloc· stringdistance_km· number · km, geo results onlyfirst_seen· datelast_seen· date
Listing detail
Passthrough columns, straight off the source feed. Display-only - not queryable.
title· string · source listing title, as publisheddealer· string · dealer name as published (dealer_netloc is the queryable form)street_address· string · selling rooftop's street addressprovince_state· string · full province/state namecountry· stringstock_number· stringengine_description· stringlisting_status· stringtitle_status· stringwindow_sticker· stringsource· string · feed the listing was crawled fromcrawled_timestamp· number · unix epoch seconds at crawl time
Pickups & fuel (raw)
Passthrough columns, straight off the source feed. Display-only - not queryable.
cab_style· string · pickups only, where the feed supplies itbed_length· string · pickups only, where the feed supplies itfuel_capacity· string · unparsed: "16 gallons", "52 liters", bare "13.2"fuel_efficiency_city· string · raw; mpg in the US, L/100km in Canadafuel_efficiency_hwy· string · raw; unit not guaranteedfuel_efficiency_cmb· string · raw; ~41% of US rows, under 3% of Canadian
Coordinates are missing on ~15% of listings
latitude/longitude come from the selling rooftop, and about 15% of the catalog has none. A geo search excludes every one of those listings, whatever else they matched, and meta.total reflects the reduced set with no separate count of what was dropped. The gap is uneven by market - under 5% in QC, over 30% in YT, NB, MB, AK and MT. To tell "outside the radius" from "location unknown", run the same search without the geo triple and compare.
Fuel-economy values have no guaranteed unit
fuel_efficiency_* is a raw source string: US listings report mpg (higher is better), Canadian listings report L/100km (lower is better), and only about half the Canadian values carry the "L/100km" suffix. A bare number is genuinely ambiguous - 8 to 25 reads plausibly on both scales - and "0" means unknown, not zero consumption. Resolve the unit from the value's own suffix, then from province_state_code, before sorting or aggregating across the border.
Claim entries on a VIN lookup
co_listings and other_claimants hold this fixed shape - every field always present, unknown values explicitly null:
id · stringdealer_site_id · string | nulldealer_netloc · string | nulldealer_id · string | nullsale_price · integer | nullfirst_seen · date | nulllast_seen · date | null…/full adds more
The /full endpoint returns the card plus these extra fields:
notesoptions[]features[]packages[]phones[]emails[]photo_urls[]history_urls[]Faceted fields
The field parameter for taxonomy/terms and autocomplete accepts:
make_namemodel_nametrim_namebody_stylefuel_namedrive_traintransmission_nameexterior_colorinterior_colorprovince_state_codecitySort values
The sort parameter on list routes accepts:
relevanceprice_ascprice_descodometer_ascodometer_descyear_descdistanceErrors
Every error shares one envelope. Branch on code - it's stable; the message is for humans and may change. Some 400s add a details[] array pinpointing the offending parameter. On 429 and 503, wait for the Retry-After header before retrying. v1.2 introduces no new error codes.
{
"error": true,
"code": "invalid_range",
"message": "year_min must be ≤ year_max.",
"details": [
{ "field": "year_min", "issue": "greater than year_max" }
]
}| Status | code | When |
|---|---|---|
| 401 | invalid_api_key | Missing, malformed, invalid, or revoked bearer key |
| 402 | insufficient_credits | Wallet balance exhausted - top up to continue |
| 402 | spend_frozen | Wallet is on a billing hold - resolve it on your account |
| 429 | rate_limited | Rate limit exceeded (per-key rps or spend velocity) - back off per Retry-After |
| 400 | invalid_query | Missing or mistyped query parameter |
| 400 | invalid_body | Malformed request body |
| 400 | invalid_geo | lat/lon/radius_km not supplied as a valid triple |
| 400 | invalid_sort | sort value not in the allowed set |
| 400 | invalid_field | field not a faceted field (taxonomy/autocomplete) |
| 400 | invalid_range | A *_min greater than its *_max |
| 400 | offset_too_deep | page × page_size beyond max_offset - traverse with cursor instead |
| 400 | invalid_cursor | Cursor malformed, sent alongside page, or replayed against a different route or sort |
| 400 | snapshot_expired | Cursor was minted against an older catalog snapshot - restart the walk |
| 400 | id_or_vin_required | comparables called without id or vin |
| 400 | batch_empty | Batch called with zero ids |
| 400 | batch_too_large | More than 100 ids on a vehicle batch, or more than 500 site ids on a resolve batch |
| 404 | vehicle_not_found | Unknown id / vin |
| 404 | dealer_not_found | Unknown dealer id / netloc / site id on an inventory route. Resolve never 404s - it answers resolution: "unknown" |
| 503 | catalog_unavailable | Snapshot not resident yet - retry per Retry-After |
| 503 | auth_unavailable | We could not reach the key resolver. Fails closed - never grants access. Retry per Retry-After |
One exception: a request that times out returns a plain 408 with no JSON body - treat any 408 as a timeout.
402 spend_frozencarries areasonofpayment_disputeorpast_due. Buying credits does not clear it - resolve the billing hold on your account.- An unknown
site_idordealer_idis not an error on search (empty result set) or on resolve (resolution: "unknown"). It is a404 dealer_not_foundon the inventory routes, which need a real target.
Metering & credits
No subscription - each request costs a little from one shared credit wallet, based on the kind of request. Run out and you get 402 insufficient_credits. The current price for each kind of request is always live at GET /v1_2/listings/meta - read it from there rather than hard-coding.
A single lookup by id or VIN - the smallest charge. Batch counts one per id.
Catalog info and stats - meta, stats, popular, taxonomy, autocomplete.
A full record with every extra field.
A filtered search or list - one charge per 100 rows returned, so a page of 1 and a page of 100 cost the same.
Note: a batch lookup is priced per id - each id is one keyed charge - and any 4xx/5xx error is refunded, so failed calls never cost credits. Two safety limits apply: 100 requests/second per key and a spend ceiling of 120 credits/minute, each returning 429 with a Retry-After.
v1.2 costs the same as v1. Every mirrored route bills exactly what its /v1 twin bills - dealer identity on a row is free. The three new dealer routes bill at the scan class, the same as dealer inventory, and resolve/batch is one scan charge per request however many of its 500 site ids you use.
Pricing
One shared wallet, no subscription - you're billed per API call by request type, and errors are never charged. Roughly 10,000 listings per credit when you page search; full records, lookups, and stats meter differently:
One shared wallet — billed per API call, not per row
| What you're doing | Per credit |
|---|---|
| Search & browsePaging search results, charged per 100 listings returned. | ~10,000search results |
| Full recordsEvery field: options, features, photos, seller contacts. | ~200full records |
| Point lookupsOne vehicle by id or VIN — batch counts one per id. | ~1,000lookups |
| Market statsRegional & YMM stats, popular, taxonomy, autocomplete. | ~500stat calls |
Approximate reach per workflow — the exact cost is metered per call at pull time, and errors are never billed.
Ready for API access?
Generate a key and start querying in minutes.