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.

Endpoints20
ProtocolREST · JSON
AuthBearer key
BillingPer call
Base URLhttps://luxe.hermesdata.io/v1_2/listings

Overview

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
Talk to us about file delivery

There is no bulk file export or download endpoint on the query API.

1
Get a key

Create one API key in your portal. It's your bearer token for every request.

2
Query the catalog

Search and filter, look up by id or VIN, or pull dealer inventory and market stats.

3
Pay per use

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_id

identifies a website

Always present

Identifies 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_id

identifies a dealership

Present when resolved

Identifies 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_id

identifies a business

Present when resolved

Identifies 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.

resolution

identifies how we linked them

Resolve responses only

How 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:

Websitedealer_site_iddealer_identity_dealer_idresolution
exampleford.com4c1f9a2e7b83d05614af2c9e3d70b8a1d_10c4f2d_10c4f2exact
exampleford.cab73e0d41c85a9f2607d1e4b39c8f5a20absentd_10c4f2redirect
examplekia.com9a4d17e6c0b23f8514ed70a9b62c4f38d_58e7b3d_10c4f2cluster
northsideauto.examplee05b6c37a91d4f28b3c7069e5a1d82f4absentabsentunknown
  • Group by dealer_site_id and you get four buckets - one per website.
  • Group by dealer_id and you get one bucket per dealership. The old domain has no dealership of its own - it only redirects - so it drops out. Group by entity_dealer_id instead 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

site ids are forever

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.

A rename is a new site

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.

One domain, one site

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:

FeatureNormallyWhile warming up
dealer_site_id on rowsPresentPresent - computed from the listing's own domain
dealer_id on rowsPresent when resolvedAbsent
entity_dealer_id on rowsPresent when resolvedAbsent
site_id search filterWorksWorks - it never needed identity
dealer_id search filterScopes to that dealership's own siteEmpty result set (200, not an error)
entity_dealer_id search filterExpands to every site of the businessEmpty result set (200, not an error)
resolve endpointsFull payloadsresolution: "unknown", netloc still echoed when we know the site
inventory by dealer_idThat dealership's own carsFalls back to the single-site v1 behavior
inventory by entity_dealer_idUnion of every site of the business404 - there is no v1 shape to fall back to
VIN claim groupsSites of one business merge into one claimEvery 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Generate 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.

Base URL

All routes live under https://luxe.hermesdata.io/v1_2/listings. Requests and responses are JSON.

Sparse records

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.

Correlation

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:

FieldUS listingCanadian listing
sale_priceUSDCAD
odometermileskilometres
distance_kmAlways 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.

Walk an entire result set
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 exclusive

Response headers

HeaderSent onMeaning
X-Request-Idevery responseCorrelation id - echoed if you send one, else generated. Quote it in support requests.
Retry-After429 · 503Seconds to wait before retrying.
X-RateLimit-Limit429Your per-key sustained request budget (requests/second).
X-RateLimit-Remaining429Requests left in the current budget after this response.
X-RateLimit-Reset429Seconds 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=3

A 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.

Request
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"
Response · 200 application/json
{
  "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.

new in v1.2Has no /v1 equivalent.richer in v1.2Same path as v1, with dealer identity added to the response (and, on search, two extra filters).same as v1Byte-identical to its /v1 twin.

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.

Recipe
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.

Recipe
# 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.

Recipe
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.

Recipe
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 present
  • dealer_id · string · the dealership entity · absent when unresolved

Identity

  • id · string · always present
  • vin · string
  • make_name · string
  • model_name · string
  • trim_name · string
  • year · integer

Specs & pricing

  • sale_price · integer · USD in the US, CAD in Canada
  • odometer · integer · miles in the US, km in Canada
  • body_style · string
  • fuel_name · string
  • drive_train · string
  • transmission_name · string
  • exterior_color · string
  • interior_color · string
  • certified · boolean
  • new_vehicle · boolean

Location & lifecycle

  • province_state_code · string
  • city · string
  • postal_code · string
  • latitude · number
  • longitude · number
  • dealer_netloc · string
  • distance_km · number · km, geo results only
  • first_seen · date
  • last_seen · date

Listing detail

Passthrough columns, straight off the source feed. Display-only - not queryable.

  • title · string · source listing title, as published
  • dealer · string · dealer name as published (dealer_netloc is the queryable form)
  • street_address · string · selling rooftop's street address
  • province_state · string · full province/state name
  • country · string
  • stock_number · string
  • engine_description · string
  • listing_status · string
  • title_status · string
  • window_sticker · string
  • source · string · feed the listing was crawled from
  • crawled_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 it
  • bed_length · string · pickups only, where the feed supplies it
  • fuel_capacity · string · unparsed: "16 gallons", "52 liters", bare "13.2"
  • fuel_efficiency_city · string · raw; mpg in the US, L/100km in Canada
  • fuel_efficiency_hwy · string · raw; unit not guaranteed
  • fuel_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_codecity

Sort values

The sort parameter on list routes accepts:

relevanceprice_ascprice_descodometer_ascodometer_descyear_descdistance

Errors

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 body · shape
{
  "error": true,
  "code": "invalid_range",
  "message": "year_min must be ≤ year_max.",
  "details": [
    { "field": "year_min", "issue": "greater than year_max" }
  ]
}
StatuscodeWhen
401invalid_api_keyMissing, malformed, invalid, or revoked bearer key
402insufficient_creditsWallet balance exhausted - top up to continue
402spend_frozenWallet is on a billing hold - resolve it on your account
429rate_limitedRate limit exceeded (per-key rps or spend velocity) - back off per Retry-After
400invalid_queryMissing or mistyped query parameter
400invalid_bodyMalformed request body
400invalid_geolat/lon/radius_km not supplied as a valid triple
400invalid_sortsort value not in the allowed set
400invalid_fieldfield not a faceted field (taxonomy/autocomplete)
400invalid_rangeA *_min greater than its *_max
400offset_too_deeppage × page_size beyond max_offset - traverse with cursor instead
400invalid_cursorCursor malformed, sent alongside page, or replayed against a different route or sort
400snapshot_expiredCursor was minted against an older catalog snapshot - restart the walk
400id_or_vin_requiredcomparables called without id or vin
400batch_emptyBatch called with zero ids
400batch_too_largeMore than 100 ids on a vehicle batch, or more than 500 site ids on a resolve batch
404vehicle_not_foundUnknown id / vin
404dealer_not_foundUnknown dealer id / netloc / site id on an inventory route. Resolve never 404s - it answers resolution: "unknown"
503catalog_unavailableSnapshot not resident yet - retry per Retry-After
503auth_unavailableWe 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_frozen carries a reason of payment_dispute or past_due. Buying credits does not clear it - resolve the billing hold on your account.
  • An unknown site_id or dealer_id is not an error on search (empty result set) or on resolve (resolution: "unknown"). It is a 404 dealer_not_found on 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.

keyed1 mc

A single lookup by id or VIN - the smallest charge. Batch counts one per id.

aggregate2 mc

Catalog info and stats - meta, stats, popular, taxonomy, autocomplete.

vdp5 mc

A full record with every extra field.

scan10 mc

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 doingPer 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.

Get API access