CarRadar

For developers

Vehicle Analytics API

The Vehicle Analytics API gives you the same market intelligence the CarRadar analytics console shows, for vehicles you describe in JSON. You send a stock list, we match each car to comparable listings across a whole market and return price bands, how the car sits against those comparables, how fast the group sells, margin potential, a price trend and the comparable listings themselves.

It is built for dealers, fleet buyers and marketplaces who want these figures inside their own systems instead of on a screen.

Getting a key

API keys are issued by hand, one per customer. Write to [email protected] with what you plan to price, roughly how many vehicles per month, and which markets. We reply with a key, your per-request price, and a webhook secret if you want one.

You do not need a paid CarRadar plan to use the API. API access is priced on its own, and a free account is enough to hold a key. If you already have an Analyst or Fleet plan, your key can be issued with the extra data those plans include.

Once your key is live, your monthly usage also appears in Account > Wallet.

Base URL and authentication

base url
https://carradar.app/api/v1

Every request carries the key as a bearer token. There are no other credentials, no signing and no session.

header
Authorization: Bearer crk_live_...
Treat the key like a password. It is stored hashed on our side and cannot be recovered, so if it leaks we revoke it and issue a new one. Keep it on your server, never in a browser or a mobile app.

How it works

Markets, not marketplaces

You price against a country, and the country's whole listing pool is used. There is deliberately no option to pick a single marketplace: a dealer competes against every listing a buyer can see, not against one site's slice of it, so a single-site median would describe a market you do not actually sell into. Pooling also handles the same physical car being advertised in several places.

marketcovers
RSSerbia
BABosnia and Herzegovina
HRCroatia
SISlovenia
DEGermany
NLBENetherlands and Belgium

Groups

Each vehicle you send is resolved to a group: the set of comparable listings it is measured against. Vehicles that resolve to the same group are computed once, which is why a batch of twenty near-identical cars is fast. The group we used is echoed back on every result, so you can always see what a number was measured against.

Mileage

Mileage is compared against a range around the odometer you send. The range we used is returned on every result as mileageMin and mileageMax, so you can always see what a figure was measured against.

Send mileage: 0 only when the car really has no kilometres on it, and leave the field out when you do not know the odometer. Those are different statements and we treat them differently: an omitted odometer applies no mileage filter at all. If you want to control the range yourself, send mileageMin and mileageMax, which are honoured exactly.

Whenever you filter by mileage, listings whose odometer is not recorded are left out of the comparison. Some marketplaces publish a zero instead of admitting the figure is missing, and counting those as low-mileage cars would distort the result.

Year banding

Send the car's own year and we resolve the comparison window from the data: that year alone when it holds enough listings, otherwise one year either side.

Widening stops at a generation change. If the neighbouring year's prices sit far enough below to indicate a different generation rather than ordinary depreciation, we refuse to widen into it and tell you so with discontinuityBlocked: true. Without that guard a facelift year drags the median down and every figure built on it is quietly wrong.

If you would rather control the window yourself, send yearFrom and yearTo instead. An explicit range is honoured exactly, with no widening.

Engines

Displacement is matched tolerantly. Marketplaces record the same engine in incompatible ways, so 1968, 1995, 2000 and 2.0 all resolve to the same comparison. Send whatever your own system holds.

Billing

One vehicle in the payload is one request. That holds however the answer is delivered and whatever it resolves to.

  • Repeats count. If two cars in the same batch land in the same group, or you send the same car again next week, each one is still a request. You get the same answer, so the price is the same.
  • Reading a job is free, and so is checking usage. You are charged for computing an answer, not for collecting one.
  • A request that fails validation is not charged, because nothing was computed.

Look up a few vehicles

POST/vehicle-api/vehicles:lookup1 request per vehicle

The direct route, for up to three vehicles. When the figures are already computed it answers in about 150 ms with the full result.

request
curl -X POST https://carradar.app/api/v1/vehicle-api/vehicles:lookup \
  -H "Authorization: Bearer crk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "market": "RS",
    "vehicles": [
      {
        "ref": "stock-1",
        "brand": "Volkswagen",
        "family": "Golf",
        "generation": "Golf 7",
        "year": 2016,
        "mileage": 145000,
        "engine": "1968",
        "fuel": "diesel"
      }
    ]
  }'
200 OK
{
  "status": "done",
  "servedInline": true,
  "vehicles": 1,
  "results": [
    {
      "ref": "stock-1",
      "cached": false,
      "group": {
        "country": "RS",
        "brand": "Volkswagen",
        "family": "Golf",
        "generation": "Golf 7",
        "yearFrom": 2014,
        "yearTo": 2016,
        "mileageMin": 125000,
        "mileageMax": 175000,
        "engine": "1968",
        "fuel": "diesel"
      },
      "yearBand": {
        "from": 2014, "to": 2016, "n": 11,
        "widened": true, "discontinuityBlocked": true
      },
      "analytics": { /* the nine blocks, shown in full further down */ }
    }
  ]
}

When the figures are not cached, this route does not make you wait. It falls back to a queued job and answers immediately with a job id, still 200. Check servedInline rather than the status code, and collect the result from the job endpoint a few seconds later.

200 OK, computed in the background
{
  "jobId": "4c9d132e-1de9-4f7c-99e2-f3106b873c68",
  "vehicles": 1,
  "groups": 1,
  "status": "queued",
  "estimatedSeconds": 2,
  "servedInline": false,
  "reason": "cold"
}
Nothing here ever blocks on a cold computation. That is what keeps one customer's large request from slowing down everyone else, including the humans using the website.

Submit a batch

POST/vehicle-api/jobs1 request per vehicle

Up to 50 vehicles per call. Returns 202 Accepted with a job id straight away; the work runs in the background. A 200-car inventory is four calls.

estimatedSeconds is an upper bound based on how many distinct groups your batch collapsed to, so it is usually pessimistic. Note the example: three vehicles, two groups, because the two X5s are close enough to be measured against the same comparables. All three are still billed.

request
curl -X POST https://carradar.app/api/v1/vehicle-api/jobs \
  -H "Authorization: Bearer crk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "market": "HR",
    "webhookUrl": "https://your-server.example/carradar",
    "vehicles": [
      { "ref": "A-101", "brand": "BMW", "family": "X5",
        "year": 2018, "mileage": 160000, "engine": "2.0",
        "fuel": "diesel", "transmission": "automatic" },
      { "ref": "A-102", "brand": "BMW", "family": "X5",
        "year": 2018, "mileage": 168000, "engine": "2.0",
        "fuel": "diesel", "transmission": "automatic" },
      { "ref": "A-103", "brand": "Volkswagen", "family": "Passat",
        "year": 2017, "mileage": 210000, "keyword": "highline -dsg" }
    ]
  }'
response
HTTP/1.1 202 Accepted

{
  "jobId": "7f1c0a2e-5d31-4a6e-9c0b-2b41d7e5a913",
  "vehicles": 3,
  "groups": 2,
  "status": "queued",
  "estimatedSeconds": 4
}

Read a job

GET/vehicle-api/jobs/{jobId}free

Returns whatever is finished so far, so a job that is half done is still useful. status moves through queued, running, then done or failed, and while it is not finished the response carries a Retry-After header telling you when to come back.

request
curl https://carradar.app/api/v1/vehicle-api/jobs/7f1c0a2e-5d31-4a6e-9c0b-2b41d7e5a913 \
  -H "Authorization: Bearer crk_live_..."
200 OK, still running
{
  "jobId": "7f1c0a2e-5d31-4a6e-9c0b-2b41d7e5a913",
  "status": "running",
  "groups": 2,
  "groupsDone": 1,
  "vehicles": 3,
  "results": [
    { "ref": "A-101", "cached": false, "group": { /* ... */ },
      "yearBand": { "from": 2017, "to": 2018, "n": 24 },
      "analytics": { /* ... */ } },
    { "ref": "A-102", "cached": true,  "group": { /* ... */ },
      "yearBand": { "from": 2017, "to": 2018, "n": 24 },
      "analytics": { /* ... */ } }
  ]
}

// A-102 shares a group with A-101, so it is marked cached and reuses the
// same computation. Both are still billed. A-103 is in the group that has
// not finished yet, which is why it is absent from results so far.

Results are keyed by your ref, never by array order, so you can join them to your own records as they arrive. Results stay readable for 7 days.

A job can only be read with the key that created it. Another customer's key gets a 404, not a 403, because confirming that an id exists is itself a leak.

Check your usage

GET/vehicle-api/usage?month=YYYY-MMfree

Defaults to the current calendar month. deduped counts the requests that reused a group another vehicle had already computed: they are still charged, and this is where you can see how many there were.

200 OK
{
  "month": "2026-08",
  "plan": "monthly",
  "calls": 7,
  "deduped": 0,
  "billedCents": 70,
  "pricePerCallCents": 10,
  "limit": 5000,
  "remaining": 4993,
  "resetsAt": "2026-09-01T00:00:00.000Z"
}

On a monthly plan, limit and remaining are filled in and requests past the limit are refused. On pay as you go both are null and there is no ceiling.

The vehicle object

Only ref, brand and family are required. Everything else narrows the comparison; the more you send, the tighter and more useful the answer.

fieldtypemeaning
refstringYour own identifier, echoed back untouched. Must be unique within one call.
brandstringManufacturer, e.g. "Volkswagen".
familystringModel family, e.g. "Golf". Not a trim or engine code.
generationstringOptional, e.g. "Golf 7". Narrows the comparison sharply when you know it.
yearintegerThe car's own year. The window is resolved from data, guarded against generation changes.
yearFromintegerExplicit window, honoured exactly. Overrides year, no widening.
yearTointegerThe other end of the explicit window.
mileageintegerOdometer in km, compared against a range around this figure. Send 0 only for a car with no kilometres on it; omit the field when the odometer is unknown.
mileageMinintegerExplicit mileage window, honoured exactly. Overrides the band from mileage.
mileageMaxintegerThe other end of the explicit window.
enginestringDisplacement in cc ("1968") or litres ("2.0"). Matched tolerantly.
fuelstring"diesel", "petrol", "hybrid", "electric".
transmissionstring"manual" or "automatic".
keywordstringFree text matched against the listing title and engine. Every token must match; a leading "-" excludes. Example: "amg" narrows a C-Class to AMG cars, "-amg" gives you everything else, and "tdi -dsg" is every TDI that is not a DSG.
trendWindowstring"3m", "6m", "1y" or "max". Defaults to 6m and is clamped to the history we actually hold.
includeLowSamplebooleanInclude thin trend buckets, each flagged lowSample. Off by default.

The keyword field is the same grammar the analytics console uses, so anything you can filter to on screen you can filter to here.

What comes back

Each result carries nine blocks. Values that used to repeat across blocks live once in group.

analytics
"analytics": {
  "group":     { "n": 11, "activeN": 5, "priceWindowDays": 30, "thinSample": false },

  "overview":  { "median": 11500, "p10": 11000, "p25": 11275, "p75": 12800, "p90": 13500,
                 "dealsN": 2, "dealsPct": 40, "velocityDays": 28, "velocityTier": "moderate",
                 "hpsAvg": 25.8, "confidence": 78, "opportunityScore": 38,
                 "decision": { "action": "AVOID", "titleText": "Avoid",
                               "summaryText": "This group is priced tightly and moving slowly...",
                               "strategyText": "Look for a different group, or wait..." },
                 "bestPocket": { "yearFrom": 2014, "yearTo": 2015,
                                 "mileageBand": { "min": 150000, "max": 175000 } } },

  "market":    { "bands": { "p10": 11000, "p50": 11500, "p90": 13500 },
                 "depth": { "supply": 5, "freshPct": 0, "stalePct": 40, "status": "undersupplied" },
                 "liquidity": { "score": 62, "grade": "moderate", "estimated": true },
                 "depreciation": [ { "year": 2016, "median": 12500, "count": 2 } ] },

  "opportunity": { "yearBand": { "from": 2014, "to": 2016, "widened": false },
                   "density": { "outstanding": 0, "strong": 2, "normal": 1, "overpriced": 2 },
                   "scatter": { "points": [ { "mileage": 175000, "price": 11000, "year": 2015 } ] },
                   "steals": [ /* the standout buys, at most 5 */ ] },

  "timing":    { "hps": { "avg": 25.8, "distribution": [40, 40, 20, 0, 0],
                          "highPressurePct": 20, "coverageN": 5 },
                 "decay": [ { "bucket": "15-30", "retentionPct": 98.3, "count": 3 } ] },

  "profit":    { "velocityDays": 28, "turnsPerMonth": 1.1,
                 "flip": { "buyPrice": 11275, "sellPrice": 11500,
                           "marginGrossPct": 2, "marginNetPct": -4.8,
                           "roiPct": -4.8, "estNetProfitEur": -545 } },

  "fleet":     { "readiness": { "score": 54, "verdict": "conditional",
                                "components": { "supply": 9, "priceIqr": 73,
                                                "yearSpan": 100, "liquidity": 71 } },
                 "window": { "signal": "neutral", "hpsAvg": 25.8, "stabilityScore": 75 },
                 "costSim": { "unitPrice": 11275, "medianPrice": 11500,
                              "negotiationPerVehicleEur": 200, "allowedCounts": [5] },
                 "uniformity": {}, "engineConfig": {} },

  "trend":     { "window": "6m", "bucket": "week", "availableWindows": ["3m", "6m"],
                 "historyStart": "2026-02-25",
                 "points": [ { "t": "2026-03-09", "median": 11999, "p25": 10625, "p75": 13700,
                               "n": 11, "dropN": 4, "exitN": 1, "ageDays": 15 } ] },

  "marketPower": { "level": "competitive", "comparableListings": 11,
                   "statsAvailable": true,
                   "text": "11 comparable listings. Enough supply for the statistics to be dependable." }
}
blockanswers
groupHow many comparables, how many still active, over what window.
overviewThe headline. Price percentiles, how many are underpriced, how fast it moves, and a plain verdict with a suggested strategy.
marketPrice bands, whether the market is over or undersupplied, how liquid it is, and a depreciation curve by year.
opportunityWhere this car sits against the comparables: a price-vs-mileage scatter, how many are underpriced or overpriced, and the standout buys.
timingSelling pressure. The High Pressure Score distribution and how listings survive by age bucket.
profitMargin. Buy price, sell price, gross and net margin, ROI and estimated profit on a flip.
fleetWhether the group can absorb a multi-car purchase: readiness score, stability, and a cost simulation.
trendPrice over time, with how many listings dropped price and how many left the market in each bucket.
marketPowerHow contested the group is, and whether the statistics are dependable.

Prices are in EUR. hps is the High Pressure Score, our measure of how hard a listing is being pushed to sell.

Thin markets are an answer, not an error

When a group has very few comparables we still return everything we have, flagged rather than withheld. A market with almost nothing in it is not missing data: it is pricing power, and it is often the most valuable thing the API can tell you.

marketPower.level is one of uncontested (nothing comparable is listed at all), monopoly (too few for a meaningful median), thin (statistics are returned but orientational) or competitive. statsAvailable tells you whether the percentiles are worth leaning on, and text is a ready sentence you can show to a user. When the statistics cannot carry the answer, we add the comparable listings themselves so you can look at the actual cars.

a group you nearly own
"marketPower": {
  "level": "monopoly",
  "comparableListings": 2,
  "statsAvailable": false,
  "text": "Only 2 comparable listings in this market. Too few for a meaningful median, which is itself the signal: you are close to setting the price rather than meeting it."
},
"listings": [
  { "siteId": "29480028", "source": "pa", "title": "BMW X5 3.0d",
    "url": "https://...", "price": 21500, "mileage": 180000,
    "year": 2016, "hps": 51, "daysListed": 12 }
]
Listing counts are capped: at most 50 scatter points and at most 5 listings in any list. You are buying analytics, not a copy of the underlying listing database, and the Terms say so explicitly.

Webhooks

Send webhookUrl with a job and we push results to it instead of making you poll. You get one call per group as it finishes and one when the job is done. A partial carries only that group's vehicles, not the whole accumulated result.

delivery
POST https://your-server.example/carradar
Content-Type: application/json
X-CarRadar-Job: 7f1c0a2e-5d31-4a6e-9c0b-2b41d7e5a913
X-CarRadar-Signature: sha256=9f2b1c...

{
  "jobId": "7f1c0a2e-5d31-4a6e-9c0b-2b41d7e5a913",
  "event": "group.completed",
  "status": "running",
  "vehicles": 3,
  "groups": 2,
  "groupsDone": 1,
  "results": [ { "ref": "A-101", "cached": false, "analytics": { /* ... */ } } ]
}

The URL must be https and must resolve to a public address. Every delivery is signed with the webhook secret issued alongside your key, as an HMAC-SHA256 of the exact bytes we sent. Verify it against the raw body, before any JSON parsing, or re-serialisation will change the bytes and the signature will never match.

verifying, node
import crypto from "node:crypto";

app.post("/carradar", express.raw({ type: "application/json" }), (req, res) => {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.CARRADAR_WEBHOOK_SECRET)
          .update(req.body)                       // the RAW bytes, not the parsed object
          .digest("hex");

  const got = req.get("X-CarRadar-Signature") ?? "";
  const ok =
    got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));

  if (!ok) return res.sendStatus(401);

  const payload = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);                            // answer fast, process after
});

Answer with any 2xx. A non-2xx is retried with exponential backoff. A webhook that is slow or down never delays your analytics: delivery runs on its own queue, and the results are always readable from the job endpoint regardless.

Claude and other AI assistants (MCP)

The same backend speaks the Model Context Protocol, so you can connect it to Claude, ChatGPT, Gemini or any other MCP client and ask about your stock in plain language instead of writing code. The assistant calls the same endpoints with the same key and the same billing.

server address
https://carradar.app/api/v1/vehicle-api/mcp

There are two ways to authenticate, and which one you use is decided by the client, not by us. A client that lets you set a request header sends your API key. A client that does not, which includes every assistant that runs in a browser, signs in through OAuth instead. Both end up on the same key, the same limits and the same ledger.

Claude in the browser

Open Settings, then Connectors, then Add custom connector, and paste the address above. Leave the OAuth fields empty: Claude registers itself automatically. Claude will send you to CarRadar to sign in, then show you what the connector is asking for and which API key it will bill. Approve it and the tools appear in your next conversation. Nothing is charged for connecting.

You need an API key before you can connect. The consent screen has to name a key to bill, so an account without one is refused there rather than failing later. Ask for a key at [email protected].

If your organisation prefers one fixed OAuth client rather than a new registration per connection, open Advanced settings in the same dialog and paste a client ID and client secret. Ask us for a pair and we will issue one scoped to your organisation. This is optional: without it, automatic registration is used and everything works the same.

Claude Code and desktop clients

These let you set an Authorization header, so they take the API key directly and skip the sign-in entirely.

command line
claude mcp add --transport http carradar \
  https://carradar.app/api/v1/vehicle-api/mcp \
  --header "Authorization: Bearer crk_live_..."
json config
{
  "mcpServers": {
    "carradar": {
      "type": "http",
      "url": "https://carradar.app/api/v1/vehicle-api/mcp",
      "headers": { "Authorization": "Bearer crk_live_..." }
    }
  }
}

To check it worked, ask the assistant what it has used this month. If it answers with a usage figure, the key is being sent correctly.

ChatGPT, Gemini and everything else

Any client that implements the MCP authorization specification connects with nothing but the address above: it discovers the rest, registers itself and runs the sign-in. If a client asks you to fill in the OAuth details by hand, these are the values it wants.

oauth details
server url             https://carradar.app/api/v1/vehicle-api/mcp
authorization url      https://carradar.app/oauth/authorize
token url              https://carradar.app/api/v1/oauth/token
registration url       https://carradar.app/api/v1/oauth/register
revocation url         https://carradar.app/api/v1/oauth/revoke
issuer                 https://carradar.app
scope                  vehicle-api
pkce                   S256, required
client authentication  none, client_secret_post or client_secret_basic

A client that cannot register itself needs a client ID, and a confidential client needs a secret with it. Ask for either at [email protected] and say which assistant it is for, so the callback address is registered correctly. Native clients that listen on a loopback address, such as Claude Code, are matched without regard to the port they happen to bind.

Nothing here needs a token in the address. A connector URL that carries a credential ends up in logs, in browsing history and in screenshots, so we do not ask for one and no assistant needs it.

What the assistant can do

tooldoescost
lookup_vehicleFull analytics for up to 3 vehicles. Answers straight away when cached.1 per vehicle
submit_vehicle_batchQueues up to 50 vehicles.1 per vehicle
get_vehicle_jobCollects a batch, including partial results.free
get_api_usageReports spend and what is left this month.free

In practice you can ask things like "what is a 2016 Golf 7 2.0 TDI with 145.000 km worth in Serbia, and is it moving?" and the assistant will fetch the figures and read them back to you.

The endpoint speaks Streamable HTTP and supports protocol revisions from 2025-03-26 through 2026-07-28, so any current MCP client works, not only Claude.

Disconnecting

Remove the connector in the assistant and it revokes its own access. Revoking the API key also ends every connection made against it, so if a key is ever exposed, revoking it is enough on its own.

One key is one customer. Whoever holds it can spend against it, and it all bills to that key, so if you need per-person accounting ask us for a key per person.

If a connector will not connect

Assistants report almost every failure as one of two sentences, so the message rarely says which step broke. These three requests do, and none of them needs a credential. Run them in order and stop at the first that does not answer as described.

check the server from anywhere
# 1. the endpoint answers, and says how to authenticate
curl -i -X POST https://carradar.app/api/v1/vehicle-api/mcp
# expect 401 with a WWW-Authenticate header naming resource_metadata

# 2. the resource metadata that header points at
curl -i https://carradar.app/.well-known/oauth-protected-resource/api/v1/vehicle-api/mcp

# 3. the authorization server it names
curl -i https://carradar.app/.well-known/oauth-authorization-server
  • Told to sign in again immediately after signing in. The account has no active API key, or the key behind the connection was revoked. Reconnecting will not fix it; ask us for a key.
  • The address was typed by hand. Use exactly the address above, with no trailing slash and no www. A connector URL that redirects loses the credential on the way.
  • Everything above answers correctly and it still fails. Send us the reference id the assistant shows, which for Claude starts with ofid_, and the time you tried, and we will match it against our own logs.

Limits and errors

limitvaluewhy
Vehicles per batch50The only cap you have to think about.
Vehicles per inline lookup3Above this, use a batch.
Batch submissions30 per minutePer key.
Inline lookups60 per minutePer key.
Job reads240 per minutePer key. Free.
Queued work100 groupsYour own backlog ceiling. Submit again once it drains.
Result retention7 daysAfter that a job id returns 404.

Rate limits are counted per key, not per IP, so several servers behind one address do not eat each other's allowance.

over the rate limit
HTTP/1.1 429 Too Many Requests

{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}
statusmeaning
400The payload is malformed. The message names the field.
401Missing, unknown or revoked key.
402Monthly plan exhausted. Usage tells you when it resets.
404No such job, or a job belonging to another key.
413Too many vehicles in one call.
429Rate limited, or your own backlog is full. The message says which. Back off and retry.

There is a short nightly window (roughly 04:45 to 05:30 CET) when our data is rebuilt and new work waits rather than starting. Jobs submitted then are accepted normally and run once it passes, so you will see a longer wait rather than an error.

Using the data

The analytics are yours to use inside your business: pricing your stock, valuing trade-ins, building your own dashboards and reports.

What you may not do is rebuild the underlying listing database or resell the raw data as a competing product. Listing counts in every response are capped for exactly this reason. The full wording is in the API section of our Terms of Service, and how we handle data sent to the API is in our Privacy Policy.

The vehicle descriptions you send are not personal data as long as you keep them that way. Do not put customer names, phone numbers, plates or VINs tied to an owner in the ref field: use your own stock number instead.