CarSearch AI · Partner API Quick start Partners site

Partner API reference

Manage the dealerships you support and drive their inventory, AI listings, part-exchange appraisals and showroom images. Every call carries your partner key and the UID of the dealership it acts on; the Authentication section explains both.

Quick start

You are a partner supporting many dealerships. You hold one partner key (rotatable, scoped); every dealership you support has a UID. Every dealership call carries both: the key says who you are, the UID says which dealership.

  1. Partner account + partner key. Sign up at the Partners portal, open API and create a secret partner key (csai_live_…). Copy it — it is shown once. It identifies you and carries your scopes; rotate or revoke it from the same page.
  2. Dealerships + their UIDs. Add a dealership — paste anything you already have (an email signature, a website footer, an address block) and confirm what it extracts. The API page lists every dealership with its UID, a copy button and a ready-made example for it; GET /v1/dealerships and POST /v1/dealerships return the same UIDs.
  3. First push — photos only. Send the dealership's photos and a stock number; the AI reads the plates and specs from the photos into the listing:
curl -X POST "https://api.car-search.ai/v1/dealership/listings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"STK1234","images":[{"url":"https://example.com/front.jpg","position":1},{"url":"https://example.com/dash.jpg","position":2}]}'

The response is 201 Created with the full listing as we hold it — its vehicle_uid, your external_id, the vehicle fields, images[] (already stored, with uids) and a pipeline block. The photos are described in the background and the listing is searchable straight away. Pushing never writes AI copy. When you want the AI-written listing, ask for it — POST /v1/dealership/listings/STK1234/enhance returns the enhanced listing in the response — then POST …/enhanced/accept (or PUT …/enhanced with your edits). Set the price when you know it: PATCH /v1/dealership/listings/STK1234 with {"price": 15995} — every update returns the same full listing. See Pushing listings and Create a listing from photos only.

  1. First embed. Create a publishable partner key (csai_pub_…, same API page — safe in page source) and drop the loader plus a component tag into the dealership's website with that dealership's UID:
<script async src="https://api.car-search.ai/widgets/loader.js"
        data-partner-key="csai_pub_YOUR_PUBLISHABLE_KEY" data-dealership="DEALERSHIP_UID"></script>
<oc-car-search></oc-car-search>

To act on your next dealership, keep the URL and the key and swap the UID. That is the whole multi-dealership story. Component tags and CSS variables are in the Embedding guide.

Authentication & keys

Every /v1 request carries your partner key. Every request that acts on a dealership also carries that dealership's UID:

Authorization: Bearer csai_live_YOUR_PARTNER_KEY
X-Dealership-Uid: DEALERSHIP_UID

Partner key — who you are, and what you may do

Two kinds, both created, named, scoped, rotated and revoked from the Partners portal (API page), both shown once and stored on our side only as a hash:

  • Secret key csai_live_… — for your servers. It is the Authorization: Bearer on every /v1 call and carries the scopes (e.g. listings:write, dealerships:read, or *) for everything you do across all of your dealerships. One per integration is a good habit. Never ship it to a browser — anyone who reads it can act as you on every dealership you support. If one leaks, rotate it: the old key stops working immediately and the replacement keeps the same name and scopes.
  • Publishable key csai_pub_… — for the dealer websites. It goes in the component snippet (data-partner-key), is safe in page source, only works from the domains of your dealerships (the union of every dealership's domain allow-list — a domain edit takes effect on the next page load, no key change) and only for the components that dealership is subscribed to. It is refused on /v1 (401 publishable_key_not_allowed).

Partner-level endpoints — GET /v1/me, listing and creating dealerships, GET /v1/jobs/{job_uid}, GET /v1/files/{uid}, webhooks — need only the secret key.

Dealership UID — which dealership

Every dealership you support has a UID. It is not a secret: it is listed with a copy button on the portal API page (Your dealerships) and on each dealership's API & embed tab, and returned by GET /v1/dealerships and POST /v1/dealerships. Send it as X-Dealership-Uid; we check it belongs to your partner account on every call. A UID that is malformed, unknown or another partner's is 404 dealership_not_found — never a 403, so nothing leaks. Leaving it out on a dealership endpoint is 400 dealership_uid_required.

Scopes

Scopes live on the partner key alone. A request outside the key's scopes returns 403 insufficient_scope with required_scope in the body. Keep a broad key for your integration and narrow keys for anything you hand to a third party.

Two URL forms

  • Header form: /v1/dealership/… + X-Dealership-Uid. Same URL for every dealership; only the header changes.
  • Path form: /v1/dealerships/{dealership_uid}/… — the UID in the URL. If you send the header too, they must match (400 dealership_uid_mismatch).

Supporting 100 dealerships

Store one partner key and a table of dealership → uid. When dealership 37's stock changes, send its UID:

for each dealership d in your table:
  curl -X POST "https://api.car-search.ai/v1/dealership/listings" \
    -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
    -H "X-Dealership-Uid: ${d.dealership_uid}" \
    -H "Content-Type: application/json" -d @listing.json

Onboarding a new dealership is POST /v1/dealerships (or the portal): the response carries its dealership_uid plus a ready-made examples.curl and examples.snippet for it — store the UID against that dealership.

Dealerships & domains

A dealership is the unit everything else hangs off: its listings, its embed components, its allowed domains. Create one by pasting whatever you already have about it — an address block, a website footer, a spreadsheet row — and the AI extracts the structured fields; you can also send them directly.

The domain allow-list (PUT /v1/dealership/domains, or Dealerships → Domains on the portal) is where the embeddable components may render: your publishable partner key is pinned to the union of every dealership's domains, evaluated on each page load, so an edit here takes effect immediately and no key changes. Keep it in sync with the domains the dealership website actually runs on.

Pushing listings

A listing is a full resource: create it, read it, update it, manage its photos, change its status, publish it, remove it. Every call returns the same object — the listing structured as we hold it:

{ "data": {
    "vehicle_uid": "0d8f3c2a-…",        // our id — use it, or your external_id, on every other call
    "external_id": "STK1234",           // yours (generated as "auto-…" if you did not send one)
    "status": "available", "published": true,
    "created_at": "…", "updated_at": "…",
    "vehicle":  { "make": "BMW", "model": "X5", "year": 2021, "vrm": "AB12CDE", "mileage": 32000, "mileage_unit": "mi",
                  "price": 15995, "currency": "GBP", "description": "…", "features": ["…"], "specs": {…}, "url_slug": "…", … },
    "images":   [{ "image_uid": "…", "position": 0, "is_primary": true, "shot_type": null, "description": null,
                   "urls": { "thumb": "https://api.car-search.ai/v1/files/…?size=thumb&dealership_uid=…", "medium": "…", "full": "…" } }],
    "pipeline": { "status": "received", "received_at": "…", "processed_at": null, "photos_total": 1, "photos_described": 0,
                  "enhanced_version": null, "enhancing": false, "error": null },
    "enhanced": null                    // { status, version, enhanced_listing_uid, source } once you have called POST …/enhance
  },
  "warnings": [] }

Create

POST /v1/dealership/listings with the listing object (JSON, or multipart with a listing JSON field plus images files) returns 201 and the full listing including its vehicle_uid. Photos — uploaded files and images[].url alike — are fetched and stored before the response, so images[] already carries their uids. You need a vrm, a vin, or make + model — or just photos (see photos only). Sending an external_id that already exists returns 409 conflict with the existing vehicle_uid; add ?upsert=1 to update that listing in place instead.

Update — by uid or by your id

{listing_id} is either the vehicle_uid we returned or your external_id; both address the same listing everywhere. PATCH …/listings/{listing_id} changes only the fields you send (a price change is instant and does not touch the AI); PUT replaces the whole object (omitted description and features are cleared). Both return the full listing. Status and visibility have their own calls: POST …/status {"status": "sold"}, POST …/publish, POST …/unpublish. DELETE removes the listing (status: "removed"): it leaves the default list, search, inventory and the website at once, but GET …/listings/{listing_id} still returns it with status: "removed" so you can confirm the delete, and GET …/listings?status=removed lists the removed ones for audit. GET …/listings lists them with status, published, q and updated_since filters — poll updated_since with your last sync time to pick up what changed. Every list answers pagination.total_count (total_results is the same number and deprecated).

Numbers are checked on the way in: price and price_was 0 to 10,000,000, mileage 0 to 2,000,000, year 1900 to next year, doors 1 to 7, seats 1 to 20, engine_size in litres (up to 20) or cc (50 to 20000). Anything else is refused with 400 validation naming the field — a negative price is never stored.

Photos

POST …/listings/{listing_id}/images (files or {"urls": […]}) appends photos; PUT …/images/order {"image_uids": […]} reorders them; POST …/images/{image_uid}/primary picks the hero image; DELETE …/images/{image_uid} removes one. Each returns the full listing. Image urls are served by GET /v1/files/{image_uid}?size=thumb|medium|full with your partner key — the dealership UID is already on the URL (?dealership_uid=, the one place a query form is accepted, so a file URL works as a plain GET). An <img> tag cannot send an Authorization header, so for a browser page exchange your publishable key + the dealership UID for a short-lived token at POST /api/embed/exchange and fetch /api/embed/images/{image_uid}/{size} with it (exactly what the embeddable components do), or proxy the bytes through your own server.

What a push does — and what it never does

  1. Stores the listing and its photos (the response already has every uid).
  2. Vectorizes it at once — the listing is in semantic search immediately, whether or not you ever ask for AI copy.
  3. Describes the new photos in the background — shot type, condition, searchable facts — in one job per listing that starts about a minute after your last write, so thirty photos sent in thirty calls are described together, each photo exactly once. pipeline.ingest_uid stays the same across those calls.

A push never writes AI copy. The enhanced listing — the AI-written headline, description, highlights, specs and features built from the original listing plus every photo description — is produced only when you call POST …/listings/{listing_id}/enhance, and that call returns it. See Enhanced listing.

pipeline.status on the listing is honest about where it is: received (stored, describing queued) → processingready (every photo described, vector current, no AI copy yet) → enhanced (only after your enhance call; enhanced_version says which) → accepted once you accept or tweak; or failed with an error. photos_described / photos_total show the describing progress. A text or spec change re-vectorizes at once; new photos are described in the background; a price-only change runs nothing. A body field enhance on any push is ignored and answered with a warning naming the enhance call.

Create a listing from photos only (AI-generated)

You do not need make, model, year or price to push a listing. The minimum is a stock number and the photos; the AI fills in what it can read from them.

  1. Push the photos. POST /v1/dealership/listings/from-photos as multipart/form-data with one images file per photo (plus an optional listing JSON field for anything you do know) — or JSON with {"external_id": "STK1234", "images": [{"url": "…", "position": 1}, …]}. The plain POST /v1/dealership/listings accepts the same photos-only body too. Everything else in the listing object is optional, even external_id (one is generated). You get 201 with the full listing — its vehicle_uid, the stored images[], empty vehicle fields, and pipeline.status: "received".
  2. The background job reads the photos. Each image is described (shot type, condition notes, tags); the registration / VIN plate and odometer are read where visible; make, model, year, colour, body, fuel, gearbox, doors and seats are proposed from what it sees. Those fill the blank fields on the listing itself (never overwriting what you sent), the listing is embedded for semantic search and pipeline.status becomes ready — read GET /v1/dealership/listings/STK1234 to see what was identified. No copy has been written yet.
  3. Ask for the AI-written listing. POST /v1/dealership/listings/STK1234/enhance returns it: headline, description, highlights, specs (make, model, year, engine, transmission… as far as the photos allowed), features, condition_notes and one image_descriptions[] entry per photo. Call it as soon as you like — if the photos are still being described it describes the rest itself.
  4. Accept or tweak. POST …/enhanced/accept publishes the AI copy and specs onto the vehicle as-is; PUT …/enhanced with your edited version stores it as source: "tweaked" and publishes that instead.
  5. Set what only you know. Price, mileage and status are yours: PATCH /v1/dealership/listings/STK1234 with {"price": 15995, "currency": "GBP", "mileage": 32000, "status": "available"}. A field-only PATCH runs nothing; new photos are described in the background; nothing but POST …/enhance writes copy.
# 1. photos only
curl -X POST "https://api.car-search.ai/v1/dealership/listings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"STK1234","images":[{"url":"https://example.com/front.jpg","position":1},{"url":"https://example.com/dash.jpg","position":2},{"url":"https://example.com/rear.jpg","position":3}]}'

# 3. ask for the enhanced listing — the response is the enhanced listing
curl -X POST "https://api.car-search.ai/v1/dealership/listings/STK1234/enhance" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID"

# 4. accept
curl -X POST "https://api.car-search.ai/v1/dealership/listings/STK1234/enhanced/accept" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID"

# 5. price it
curl -X PATCH "https://api.car-search.ai/v1/dealership/listings/STK1234" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" -d '{"price":15995,"currency":"GBP"}'

If you do know the make, model or price up front, include them — they are used as given and the AI only fills the gaps. Unknown keys are dropped and echoed back in warnings[].

Enhanced listing: ask for it, then accept or tweak

You ask for the enhanced listing; it is never generated behind your back. POST /v1/dealership/listings/{listing_id}/enhance runs the whole pass synchronously — any photo not yet described is described (in parallel), specs are extracted from every photo write-up, the copy is written, the listing is re-vectorized, a new proposed version is stored — and the response is the enhanced listing (200, the same object GET …/enhanced returns, plus pipeline): headline, description, highlights, specs, features, condition_notes, one image_descriptions[] entry per photo and original. A listing with a normal number of photos comes back in well under a minute.

curl -X POST "https://api.car-search.ai/v1/dealership/listings/STK1234/enhance" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID"

{ "data": { "version": 1, "status": "proposed", "headline": "…", "description": "…", "highlights": […], "specs": {…},
            "features": […], "image_descriptions": [{ "image_uid": "…", "shot_type": "front_quarter", "description": "…" }],
            "original": {…}, "pipeline": { "status": "enhanced", "enhanced_version": 1, … } },
  "unchanged": false, "warnings": [] }
  • The photos disagree with what you sent? When every exterior shot shows a red car and the push said colour: "Blue" (the same for body_type, make, model), your data is kept as sent — nothing is rewritten — the copy is written without asserting that attribute, and data.warnings[] names the field, what you declared and what the photos show (the plain-text warnings[] on the enhance call carries the same line). GET …/enhanced returns the same warnings for as long as that version is current, so fix the listing and enhance again.
  • Highlights are 3–6 short, distinct selling points; the features list is separate and is not restated inside them.
  • Nothing changed? It is free. Call enhance again with no photo added or removed and no text or spec edited and you get the current version back — proposed, accepted or your tweaked one — with unchanged: true; no AI work runs and nothing is charged. Send {"identify": true} to force a fresh pass that also re-reads plate / VIN / odometer and proposes specs into the blank fields.
  • Too many photos for one request? Only when the photos cannot all be described inside the request budget does the call answer 202 with the full listing (pipeline.status: "processing", pipeline.enhancing: true) and finish in the background — poll GET …/enhanced or take the listing.enhanced.ready webhook, which fires in both cases. This is the exception, not the path.
  • Until you call it, GET …/enhanced is 404 not_generated and the listing's enhanced is null.

Then you have two choices:

  • Accept (POST .../enhanced/accept) — apply the AI copy to the live listing as-is. Returns the full listing with enhanced.status: "accepted".
  • Tweak (PUT .../enhanced) — edit it on your end first, then send back the final version. It's stored as a new version (source: "tweaked") and applied; the full listing comes back.

Every version is kept, so you can always see what the AI originally proposed versus what was published. Changed the photos or the copy since? Call POST …/enhance again — it describes only the new photos and returns the next version.

Part exchange flow

A part-exchange (trade-in) appraisal is four calls: start → photos → process → get. Every call carries the partner key and the dealership UID.

  1. Start. POST /v1/dealership/part-exchange with {"contact": {"first_name", "last_name", "email", "phone"}, "vehicle": {"reg" | "vin", "mileage"}}. The contact becomes a CRM lead on the dealership; the reply is 201 with part_exchange_uid (and an upload_token you can hand to a browser-side uploader).
  2. Photos. POST …/part-exchange/{id}/photos as multipart/form-data, one photos file per shot — registration/VIN plate, dashboard (mileage + warning lights), the four exterior corners, interior. Each photo is reviewed by AI as it arrives: the reply names the shot it recognised (ai_shot_type, confidence) and what is still missing from the checklist.
  3. Process. POST …/part-exchange/{id}/process runs the appraisal now (it would otherwise run automatically shortly after the last photo). Reply 202 with a job_uid; poll GET /v1/jobs/{job_uid} until done.
  4. Get the appraisal. GET …/part-exchange/{id} returns the identified vehicle (from the plate and dashboard), condition notes per shot, the valuation range and the lead it was attached to.
curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"contact":{"first_name":"Sam","last_name":"Jones","email":"sam@example.com","phone":"07700900123"},"vehicle":{"reg":"AB12CDE","mileage":41000}}'

curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID/photos" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "photos=@plate.jpg" -F "photos=@dash.jpg" -F "photos=@front-left.jpg"

curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID/process" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID"

curl "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID"

Upload showroom backdrops and generate showroom listing images

Turn a photo taken on the forecourt into a showroom shot: the car is cut out, placed on a backdrop and lighting-harmonised. Five steps, every call with the partner key and the dealership UID.

  1. Upload a backdrop (once per dealership, or use ours). POST /v1/dealership/showroom-backdrops as multipart/form-data with one or more images files (JPEG/PNG/WebP). Reply 201 with a backdrop_uid per file. GET /v1/dealership/showroom-backdrops lists the dealership's own backdrops plus the platform defaults (kind: "platform").
  2. Start the generation job. POST /v1/dealership/showroom-images with the source photo — {"image_uid": "…"} for a photo already on a vehicle, {"url": "…"}, or a multipart image file — plus backdrop_uid, optionally vehicle_uid and options (e.g. {"width_fraction": 0.7}). Reply 202 with a job_uid.
  3. Poll the job. GET /v1/jobs/{job_uid} reports queuedrunningdone (or failed with a reason). When done, output carries the size and timings.
  4. Fetch the image. GET /v1/files/{job_uid} streams the JPEG (add ?inline=1 on the job endpoint to get it base64-inlined instead).
  5. Accept it onto the vehicle. POST /v1/dealership/showroom-images/{job_uid}/accept (optionally {"vehicle_uid": "…"}) attaches the result as a vehicle image — it then appears in the listing's images[] and is served like any other vehicle photo (GET /v1/files/{image_uid}?dealership_uid=…&size=thumb).
curl -X POST "https://api.car-search.ai/v1/dealership/showroom-backdrops" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@showroom-backdrop.jpg"

curl -X POST "https://api.car-search.ai/v1/dealership/showroom-images" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"image_uid":"IMAGE_UID","backdrop_uid":"BACKDROP_UID","vehicle_uid":"VEHICLE_UID","options":{"width_fraction":0.7}}'

curl "https://api.car-search.ai/v1/jobs/JOB_UID" -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
curl "https://api.car-search.ai/v1/files/JOB_UID" -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -o showroom.jpg

curl -X POST "https://api.car-search.ai/v1/dealership/showroom-images/JOB_UID/accept" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" -d '{"vehicle_uid":"VEHICLE_UID"}'

Semantic search

POST /v1/dealership/search interprets a shopper's free text ("economical family SUV under 20k") into filters plus a ranked semantic match, and tells you what it understood via interpretation. The legacy GET /api/v1/search (filters only, no AI hop) is faster for callers on a strict timeout, such as a voice platform.

Chat: we keep the context

Send a message, get a reply — that is the whole integration. We store the entire conversation server-side and rebuild the model's context ourselves on every turn (a sliding window of recent turns, older ones compacted) so you never assemble or re-send history. Every response carries a conversation_uid; send it back on the next call and nothing else changes.

Two calls, one thread

# 1. first message — no conversation_uid yet
curl -X POST "https://api.car-search.ai/v1/dealership/chat" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"message":"Looking for a family SUV, budget around £20k"}'
# → { "data": { "conversation_uid": "9c2e...", "message": "...", "suggestions": ["..."], "vehicles": [...], "tool_calls": [...] } }

# 2. next message — ONLY the new text + the conversation_uid; the £20k budget is remembered
curl -X POST "https://api.car-search.ai/v1/dealership/chat" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"message":"Does it come in automatic?","conversation_uid":"9c2e..."}'

message is the assistant's reply text; suggestions[] are short tappable follow-ups; vehicles[] is present only when the answer referenced stock; tool_calls[] lists what the assistant did behind the scenes (name, label, status, duration_ms). An older history[] array is still accepted on the very first call for callers migrating off client-managed history, but is ignored the moment a conversation_uid exists — the stored conversation is always authoritative.

Streaming: watch it think

POST /v1/dealership/chat/stream sends the same turn as Server-Sent Events, so your UI can show what the assistant is doing instead of a blank wait:

EventShapeWhen
meta{ conversation_uid }Once, immediately
status{ state, label, tool }state is thinking / searching / looking_up / writing; tool names which tool if any (e.g. search_inventory) — drive your "Searching…" indicator off this
tool{ name, status, result_count }A tool call just finished — turn the indicator back off
delta{ text }Each chunk of the reply's text, in order
done{ message, suggestions, conversation_uid, vehicles, tool_calls }Terminal — the full reply plus everything the non-streaming call returns
const res = await fetch('https://api.car-search.ai/v1/dealership/chat/stream', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer csai_live_YOUR_PARTNER_KEY',
    'X-Dealership-Uid': 'DEALERSHIP_UID',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ message, conversation_uid }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  let idx;
  while ((idx = buf.indexOf('\n\n')) !== -1) {
    const line = buf.slice(0, idx).replace(/^data: /, '');
    buf = buf.slice(idx + 2);
    const ev = JSON.parse(line);
    if (ev.type === 'status') showThinking(ev.label);        // e.g. "Searching stock…"
    else if (ev.type === 'tool') hideThinking();
    else if (ev.type === 'delta') appendText(ev.text);
    else if (ev.type === 'done') { conversation_uid = ev.conversation_uid; showSuggestions(ev.suggestions); }
  }
}

Don't want to build any of this yourself? The embedded <oc-ai-chat> component (see the Embedding guide) already does — thinking indicator, streaming text, suggestion chips and conversation continuity included.

AI cost control and per-dealership settings

Every dealership you support has its own assistant, and you decide what it costs to run and how it behaves. Two endpoints do the whole job: one reads and writes the settings, the other reports the spend. Money is US dollars throughout, the currency the models are billed in, and nothing is converted.

Set the cost per conversation

This is the one that decides the bill. A conversation may spend up to this much; when it gets there the assistant stops answering that chat and asks the shopper to contact the sales team. Two cents is a short, useful chat on the cheapest model.

curl -X PUT "https://api.car-search.ai/v1/dealership/ai/settings"   -H "Authorization: Bearer csai_live_YOUR_SECRET_KEY"   -H "X-Dealership-Uid: DEALERSHIP_UID"   -H "Content-Type: application/json"   -d '{"session_cost_cap_usd": 0.02, "daily_cost_cap_usd": 5}'

Inject your own instructions

system_instructions is added to the assistant instructions for that one dealership, on every reply of its embedded chat. No other dealership sees it. Write house rules, tone, what to push, what never to say.

curl -X PUT "https://api.car-search.ai/v1/dealership/ai/settings"   -H "Authorization: Bearer csai_live_YOUR_SECRET_KEY"   -H "X-Dealership-Uid: DEALERSHIP_UID"   -H "Content-Type: application/json"   -d '{"system_instructions": "Always mention our seven day money back promise. Never quote a finance rate; offer to have the finance team call."}'

What you can set, and what happens if you do not

A write is partial: only the fields you send change. Sending null on a field that allows it puts that field back to the standard behaviour, so {"session_cost_cap_usd": null} returns to fifty cents a conversation while {"session_cost_cap_usd": 0} means no cap at all. GET /ai/settings also returns effective, which is the same table resolved for you, and fields, the machine-readable version of it.

FieldRangeLeft blank
session_cost_cap_usd
cost per conversation
0 to 100 USD$0.50 per conversation. Zero means no limit.
daily_cost_cap_usd0 to 10000 USDNo daily limit. Zero also means no limit.
session_msg_limit0 to 50050 messages from the shopper. Zero means no limit.
max_tokens256 to 81922048 tokens per reply. A dealership created through the portal starts at 4096.
model_idA model we serveclaude-haiku-4-5-20251001
temperature0 to 1Left to the model.
assistant_name80 charactersIt calls itself "the assistant".
welcome_message1000 characters"Hi! How can I help you find your next car?"
system_instructions20000 charactersNo extra instructions.
widget_enabledtrue or falseOn. Turn it off and the chat stops answering on the dealer website.

Read the spend

What the numbers mean: conversations counts conversations that had a shopper or assistant message inside the window (per day: on that day); messages counts the shopper's and the assistant's turns — the assistant's internal tool steps are not messages, though their cost is included. The per-conversation messages, the per-day figures and the totals all use that one definition.

GET /v1/ai/usage is partner-level: no dealership uid, no header, one row per dealership. GET /v1/dealership/ai/usage takes the header and goes down to the individual conversations. Both take from and to as YYYY-MM-DD, or days for the last N days.

curl "https://api.car-search.ai/v1/ai/usage?days=30"   -H "Authorization: Bearer csai_live_YOUR_SECRET_KEY"
{
  "data": {
    "totals": {
      "from": "2026-08-14", "to": "2026-09-12", "currency": "USD",
      "conversations": 412, "messages": 3688,
      "input_tokens": 2214880, "output_tokens": 501233,
      "cost_usd": 9.4187, "avg_cost_per_conversation_usd": 0.0229,
      "conversation_cap_hits": 14, "daily_cap_days": 1, "message_cap_hits": 0
    },
    "dealerships": [
      {
        "dealership_uid": "2e78b4d8-3724-46af-9638-f1b7221728a6",
        "name": "Aylesbury AI Motors",
        "conversations": 210, "messages": 1904,
        "input_tokens": 1120441, "output_tokens": 254008,
        "cost_usd": 5.2011, "avg_cost_per_conversation_usd": 0.0248,
        "conversation_cap_hits": 11, "daily_cap_days": 1,
        "session_cost_cap_usd": 0.02, "daily_cost_cap_usd": 5, "widget_enabled": true
      }
    ],
    "days": [
      { "date": "2026-09-12", "cost_usd": 0.3411, "conversations": 15, "messages": 132 }
    ]
  }
}

conversation_cap_hits counts the chats the cost cap actually stopped, recorded at the moment of the stop, so raising the cap later does not rewrite last month. In the per-dealership report each conversation carries stopped_by_conversation_cap and its own cost_usd, and its conversation_uid is the one the Conversations endpoints read, so you can go straight from an expensive chat to its transcript.

Reads need the ai:read scope on your partner key; writes need ai:write. A dealership that is not yours is 404, never 403.

The CRM: everything the AI captured

Everything the AI captured — leads, contacts, part-exchanges, appointments — is yours through the API; the same records the dealership sees. When the assistant takes a shopper's details in chat, when someone runs a part-exchange on the dealer's website, when a form is filled in, one record is written into that dealership's CRM. There is no second copy for partners: you read and write the same rows its sales team works from, so a stage you set here is the stage they see.

Read the leads

Two headers, as everywhere else — the key says who you are, the UID says which dealership:

curl "https://api.car-search.ai/v1/dealership/crm/leads?source=ai_chat&limit=20"   -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"   -H "X-Dealership-Uid: DEALERSHIP_UID"

Each lead carries the contact, the car they asked about, the car they want to trade in, and an ai block with the assistant's summary of the conversation, an intent score and what the shopper said they were looking for. source.conversation_url is a ready-made link to the full transcript at GET /v1/dealership/conversations/{conversation_uid}.

FilterValuesUse it for
sourceai_chat · part_exchange · form · api · semantic_search · otherOnly what the AI assistant captured, only trade-ins, and so on
status / stage_uidopen · won · lost / a uid from /crm/pipelineYour own board, mirroring the dealership's stages
updated_sinceISO timestampPolling: keep the newest updated_at you saw and pass it back
qfree textName, company, email, phone, make, model, reg

Work them

GET /crm/leads/{lead_uid} returns the whole record — stage history, timeline, notes, appointments and tasks. PATCH moves it the way the dealership's own CRM does (a terminal stage marks the lead won or lost, and the change lands on the timeline), and a note is appended, never overwritten:

curl -X PATCH "https://api.car-search.ai/v1/dealership/crm/leads/LEAD_UID"   -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"   -H "X-Dealership-Uid: DEALERSHIP_UID"   -H "Content-Type: application/json"   -d '{"stage":"Contacted","appointment_at":"2026-09-14T10:30:00Z","note":"Booked a viewing for Saturday."}'

The stage is the name exactly as the dealership has it. Every dealership starts with New Lead, Contacted, Appointment Set, Showroom Visit, Test Drive, Write-Up/Negotiation, F&I, Delivered/Won, Lost/Unsold; read GET /crm/pipeline for the live list and send either the name or the stage_uid. An unknown name is a 400 whose message lists the valid stages.

POST /crm/leads pushes one in from your side (it lands with source.type = api), and POST /crm/leads/{lead_uid}/notes adds a note on its own.

Contacts, part-exchanges, appointments, pipeline

  • GET /crm/contacts and /crm/contacts/{contact_uid} — the person, every lead of theirs and every AI conversation they had, each linked to its transcript.
  • GET /crm/part-exchanges and /{part_exchange_uid} — the customer's car as identified, every photo with what the AI read off it (shot type, description, plate, VIN, odometer), the condition notes and the valuation. A part-exchange is also a lead and shares its uid.
  • GET /crm/appointments — the viewings, test drives and callbacks booked on those leads; defaults to everything from now onwards.
  • GET /crm/pipeline — each dealership names its own stages; these are the stage_uids to filter and move leads with.

Every list takes offset and limit and returns pagination.total_count. A lead, contact or part-exchange belonging to another dealership is 404, never 403. Reads need the crm:read scope on your partner key; writes need crm:write.

Do not poll, subscribe

Register an https endpoint once and we push lead.created, lead.updated and part_exchange.created as they happen, each carrying the uids and a ready-made link back to the record:

curl -X POST "https://api.car-search.ai/v1/webhooks"   -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"   -H "Content-Type: application/json"   -d '{"url":"https://your-app.example/hooks/carsearch","events":["lead.created","lead.updated","part_exchange.created"]}'

The response carries the signing secret once. The webhooks guide has the whole story: the event catalogue, the signature, the retry schedule and how to check a message really came from us.

Webhooks: get told, do not poll

Everything the AI captures for a dealership lands in that dealership's CRM on our side. A webhook is how your systems find out the moment it happens, so nothing has to sit on a timer waiting for a lead that may never come.

Give us one https address and choose the events you want. A webhook belongs to you, not to one dealership: a single endpoint can receive events for every dealership you support, or you can name the ones you want.

curl -X POST "https://api.car-search.ai/v1/webhooks" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://your-app.example/hooks/carsearch",
        "description": "Our CRM inbox",
        "events": ["lead.created", "lead.updated", "part_exchange.created"]
      }'

The response carries a signing secret starting whsec_. It is shown once. Store it before you close the response; if you lose it, POST /v1/webhooks/{webhook_uid}/rotate-secret gives you a new one and the old one stops working straight away.

Everything here is on the Webhooks page of the partner portal too, including a Send test button and the delivery log.

What a message looks like

A body carries the uids you need to route the event and a links object with the exact API path to read the full record. It never carries a customer's name, email, phone or the chat transcript. A body travels to an address we do not control and ends up in log files at both ends; the record itself is read back with your partner key, which is authenticated, scoped and revocable.

POST https://your-app.example/hooks/carsearch
Content-Type: application/json
User-Agent: CarSearchAI-Webhooks/1
X-CSAI-Event: lead.created
X-CSAI-Delivery: 9302766f-fef9-4001-b256-e17f3de2004b
X-CSAI-Signature: t=1789198827,v1=fc5e2f4343bd9c444d2cec03141133daf35bc423f117e5396

{
  "event": "lead.created",
  "delivery_uid": "9302766f-fef9-4001-b256-e17f3de2004b",
  "occurred_at": "2026-09-12T07:40:26.433Z",
  "dealership_uid": "c3cd12ef-82d7-45e9-b90a-980ddaeda12d",
  "data": {
    "lead_uid": "7dff8ffd-5aa0-45be-ac87-854a2f475935",
    "contact_uid": "742db05c-3925-418c-b24d-ae3c7dc9a6b1",
    "conversation_uid": null,
    "vehicle_uid": null,
    "source": "ai_chat",
    "enquiry_type": "sales",
    "status": "open",
    "stage_name": "New enquiry"
  },
  "links": {
    "lead": "/v1/dealerships/c3cd12ef-82d7-45e9-b90a-980ddaeda12d/crm/leads/7dff8ffd-5aa0-45be-ac87-854a2f475935",
    "contact": "/v1/dealerships/c3cd12ef-82d7-45e9-b90a-980ddaeda12d/crm/contacts/742db05c-3925-418c-b24d-ae3c7dc9a6b1"
  }
}

Every path in links is relative to https://api.car-search.ai and works with the partner key you already have, so there is no second call to work out where the record lives.

The events

GET /v1/webhooks/events returns this table as JSON, with a sample body for each one. Read it rather than hard-coding the list: subscribing to "*" also gets you events we add later.

EventWhen it firesUids it carries
conversation.startedA shopper opened a chat with the AI assistant on the dealership website.conversation_uid
conversation.endedA chat finished. It carries how many messages were exchanged and whether a lead came out of it.conversation_uid lead_uid
conversation.summarisedThe AI has written its summary of a chat and scored how ready the shopper is to buy. Read the summary from the conversation link.conversation_uid lead_uid
lead.createdA new lead reached the dealership CRM, from the AI chat, a website form or your own API call.lead_uid contact_uid conversation_uid
lead.updatedA lead moved stage, changed status, was assigned to someone or was closed.lead_uid contact_uid
part_exchange.createdA customer offered a car in part exchange, from the sell-your-car page, the AI chat or your own API call.part_exchange_uid lead_uid contact_uid conversation_uid
part_exchange.photos_reviewedThe AI has looked at the photos of the car being offered and written up what it can see.part_exchange_uid lead_uid vehicle_uid
part_exchange.appraisedThe part exchange has a valuation on it. Read the amount from the part exchange link.part_exchange_uid lead_uid vehicle_uid
listing.enhanced.readyAn enhanced listing you asked for (POST …/listings/{id}/enhance) is ready — the call itself returns it; this fires as well, and is how you learn a 202 (finished in the background) completed.vehicle_uid job_uid
showroom.readyA showroom image you asked for has finished rendering and can be downloaded.job_uid vehicle_uid

Choosing what you get

A subscription takes any number of events, or "*" for all of them. It covers every dealership you support by default, or name the ones you want with dealership_uids:

curl -X PATCH "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["*"],"dealership_uids":["DEALERSHIP_UID_ONE","DEALERSHIP_UID_TWO"]}'

POST /v1/webhooks/{webhook_uid}/pause stops sending without losing the endpoint, its events or its secret. /resume starts it again.

Send yourself a test

POST /v1/webhooks/{webhook_uid}/test sends a real, signed delivery of a sample body while you wait, and answers with exactly what came back: the status, how long it took and the first kilobyte of the response. It appears in the delivery log marked as a test, it is never retried, and it never counts toward the failures that disable an endpoint.

curl -X POST "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/test" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"lead.created"}'

{ "data": { "delivered": true, "status_code": 200, "duration_ms": 48,
            "response_body": "{\"ok\":true}",
            "message": "Your endpoint answered 200 in 48 ms." } }

Checking a message really came from us

X-CSAI-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256>. v1 is keyed with your signing secret over the text t plus a full stop plus the raw body exactly as it arrived. Read the raw bytes before any JSON parsing: re-serialising the body changes it and the check will fail. Compare with a constant time comparison, and reject anything where t is more than five minutes old so an old message cannot be replayed at you.

const express = require('express');
const crypto = require('crypto');
const app = express();

const SECRET = 'whsec_YOUR_SIGNING_SECRET';

// The RAW body is what was signed, so read it as bytes and parse afterwards.
app.post('/hooks/carsearch', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-CSAI-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  const v1 = String(parts.v1 || '');

  // Reject anything older than five minutes so nobody can replay it.
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).send('stale');

  const expected = crypto.createHmac('sha256', SECRET)
    .update(t + '.' + req.body.toString('utf8'))
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(400).send('bad signature');

  const event = JSON.parse(req.body.toString('utf8'));
  // Answer first, work afterwards: we wait ten seconds and no longer.
  res.sendStatus(200);
  queue.add(event);   // event.event, event.data, event.links
});
<?php
$secret = 'whsec_YOUR_SIGNING_SECRET';

// The RAW body is what was signed. Read it before anything parses it.
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_CSAI_SIGNATURE'] ?? '';

$parts = [];
foreach (explode(',', $header) as $bit) {
    [$k, $v] = array_pad(explode('=', $bit, 2), 2, '');
    $parts[$k] = $v;
}
$t = (int)($parts['t'] ?? 0);
$v1 = $parts['v1'] ?? '';

// Reject anything older than five minutes so nobody can replay it.
if (!$t || abs(time() - $t) > 300) { http_response_code(400); exit('stale'); }

$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);
if (!hash_equals($expected, $v1)) { http_response_code(400); exit('bad signature'); }

$event = json_decode($raw, true);

// Answer first, work afterwards: we wait ten seconds and no longer.
http_response_code(200);
flush();

// $event['event'], $event['data'], $event['links']
handle_event($event);

When your endpoint is down

Answer with any 2xx status within ten seconds and we count it as delivered. Anything else and we try again after 2, 4, 8 then 16 minutes, five attempts in all, which is about half an hour. Answer quickly and do your work afterwards: put the message on your own queue and return 200 straight away.

If ten messages in a row do not get through we stop sending and mark the endpoint as stopped, with the reason in disabled_reason and on the Webhooks page of the portal. POST /v1/webhooks/{webhook_uid}/resume starts it again and clears the failure run. Events that happen while an endpoint is stopped or paused are not kept.

The same delivery can arrive twice, for example when your answer was lost on the way back to us. Treat X-CSAI-Delivery as the key and ignore one you have already handled.

DELETE /v1/webhooks/{webhook_uid} removes an endpoint: it leaves your list at once and nothing more is sent to it, but every delivery it ever had stays readable for audit — GET /v1/webhooks/deliveries?webhook_uid=… and GET /v1/webhooks/deliveries/{delivery_uid} keep answering. A deleted endpoint cannot be updated, paused, tested or given a new secret, and a retry of one of its deliveries is refused.

The delivery log

GET /v1/webhooks/deliveries is every delivery across all of your endpoints, newest first, filterable by webhook_uid, event, status and dealership_uid, and paged with offset and limit. GET /v1/webhooks/deliveries/{delivery_uid} adds the exact request we sent, its headers, and what your endpoint answered; the signing secret is never in it. POST /v1/webhooks/deliveries/{delivery_uid}/retry sends one again immediately instead of waiting for the next scheduled attempt.

Managing webhooks needs the webhooks:manage scope on your partner key. A webhook or delivery belonging to another partner is 404, never 403.

Jobs

Showroom-image generation and part-exchange processing run as background jobs: the triggering call returns 202 with a job_uid, and GET /v1/jobs/{job_uid} reports queuedrunningdone/failed plus the output. A large result (like the showroom image bytes) is fetched separately from GET /v1/files/{job_uid} once the job is done, or inlined as base64 with ?inline=1.

You do not have to poll. Register an endpoint once and we push showroom.ready, part_exchange.photos_reviewed, part_exchange.appraised and listing.enhanced.ready (when an enhance you requested is ready — useful for the 202 case) the moment each one finishes. See Webhooks.

Rate limits & errors

Every key is limited to 300 requests/minute by default. Going over the limit returns 429 with a Retry-After header (seconds) — back off and retry rather than hammering.

Every error is JSON, shaped the same way everywhere — a stable code to branch on, and a human-readable message:

{ "error": { "code": "validation", "message": "external_id is required" } }
StatusCodesMeaning
400validation, bad_multipart, too_many_filesInvalid request body or parameters. An out-of-range number names the offending field (and lists every problem under errors[])
400dealership_uid_required, dealership_uid_mismatchA dealership endpoint was called without X-Dealership-Uid (and without the UID in the path), or the header and the path name different dealerships
401unauthorized, invalid_key, publishable_key_not_allowedMissing, malformed, revoked or expired partner key — or a publishable key (csai_pub_) used on the API instead of the secret key
403insufficient_scope, partner_suspendedThe partner key lacks the required scope (required_scope is in the body), or the partner account is suspended
404dealership_not_found, not_foundThe dealership UID is malformed, unknown or belongs to another partner (never a 403, so nothing leaks); or the listing / job / file does not exist
409conflict, not_ready, pipeline_failed, no_outputThe external_id already exists (the body carries its vehicle_uid; use PATCH or ?upsert=1), not ready yet, or the pipeline/job failed — see the listings, enhanced-listing and jobs guides
413file_too_large, request_too_largeAn uploaded file exceeded 15 MB, or one request carried more than 120 MB in total. Answered the moment the limit is crossed — from Content-Length before any body is read when it is declared, otherwise mid-stream — and the connection is closed, so nothing more is read
429rate_limited, photo_limitRate limit exceeded — see Retry-After
5xxserver_errorUnexpected server error — safe to retry with backoff

Embedding guide

Four white-label components can be dropped onto any dealer website you support: semantic inventory search, the AI sales chat, part-exchange capture, and AI showroom-image generation. Load the component library once per page, with your publishable partner key and the dealership's UID:

<script async src="https://api.car-search.ai/widgets/loader.js"
        data-partner-key="csai_pub_YOUR_PUBLISHABLE_KEY"
        data-dealership="DEALERSHIP_UID"></script>

data-partner-key is a publishable partner key (csai_pub_…, Partners portal → API → create a publishable key). It is safe to expose in page source: it only works from the domains of your dealerships (each dealership's Domains allow-list) and only for the components that dealership is subscribed to, and it is refused on the /v1 API. Never put a secret key (csai_live_…) in a page — the loader refuses it and you should rotate it. data-dealership is the dealership's UID (Partners portal → API → Your dealerships, copy button next to each): it decides whose inventory the components show. One snippet pattern, one key, a different UID per dealer site — the portal's Your dealerships table hands you the finished snippet per dealership. A page may also set them per element (<oc-ai-chat partner-key="…" dealership="…">) when it mixes dealerships.

Semantic inventory search

<oc-car-search></oc-car-search>

AI sales chat

<oc-ai-chat></oc-ai-chat>

Part exchange / trade-in capture

<oc-part-exchange></oc-part-exchange>

Showroom image generator

<oc-showroom></oc-showroom>

Style any component with CSS variables — set them on :root, a wrapper, or the element itself:

oc-car-search {
  --oc-primary: #c8102e;
  --oc-radius: 10px;
}

CSS variables per component

Loaded live from scripts\seed-partner-components.mjs (14 shared + per-component variables).

<oc-car-search> — Semantic inventory search

VariableDefaultDescription
--oc-primary#1d4ed8Brand colour — buttons, links, highlights, header bars.
--oc-primary-contrast#ffffffText/icon colour on top of --oc-primary.
--oc-bgtransparentBackground behind the component (transparent = inherit the page).
--oc-surface#ffffffCards, panels and inputs.
--oc-surface-alt#f8fafcSecondary surfaces — transcript area, placeholders, chips.
--oc-border#e2e8f0Borders and dividers.
--oc-text#0f172aBody text.
--oc-muted#64748bSecondary text — hints, specs, counts.
--oc-radius12pxCorner radius of cards and panels.
--oc-radius-sm9pxCorner radius of buttons and inputs.
--oc-fontsystem-ui, 'Segoe UI', Arial, sans-serifFont family for the whole component.
--oc-success#15803dPositive states — in stock, photo accepted, saved.
--oc-warning#b45309Attention states — reserved, AI flagged a photo.
--oc-danger#b91c1cErrors and sold badges.

<oc-ai-chat> — AI sales chat

VariableDefaultDescription
--oc-primary#1d4ed8Brand colour — buttons, links, highlights, header bars.
--oc-primary-contrast#ffffffText/icon colour on top of --oc-primary.
--oc-bgtransparentBackground behind the component (transparent = inherit the page).
--oc-surface#ffffffCards, panels and inputs.
--oc-surface-alt#f8fafcSecondary surfaces — transcript area, placeholders, chips.
--oc-border#e2e8f0Borders and dividers.
--oc-text#0f172aBody text.
--oc-muted#64748bSecondary text — hints, specs, counts.
--oc-radius12pxCorner radius of cards and panels.
--oc-radius-sm9pxCorner radius of buttons and inputs.
--oc-fontsystem-ui, 'Segoe UI', Arial, sans-serifFont family for the whole component.
--oc-success#15803dPositive states — in stock, photo accepted, saved.
--oc-warning#b45309Attention states — reserved, AI flagged a photo.
--oc-danger#b91c1cErrors and sold badges.

<oc-part-exchange> — Part exchange / trade-in capture

VariableDefaultDescription
--oc-primary#1d4ed8Brand colour — buttons, links, highlights, header bars.
--oc-primary-contrast#ffffffText/icon colour on top of --oc-primary.
--oc-bgtransparentBackground behind the component (transparent = inherit the page).
--oc-surface#ffffffCards, panels and inputs.
--oc-surface-alt#f8fafcSecondary surfaces — transcript area, placeholders, chips.
--oc-border#e2e8f0Borders and dividers.
--oc-text#0f172aBody text.
--oc-muted#64748bSecondary text — hints, specs, counts.
--oc-radius12pxCorner radius of cards and panels.
--oc-radius-sm9pxCorner radius of buttons and inputs.
--oc-fontsystem-ui, 'Segoe UI', Arial, sans-serifFont family for the whole component.
--oc-success#15803dPositive states — in stock, photo accepted, saved.
--oc-warning#b45309Attention states — reserved, AI flagged a photo.
--oc-danger#b91c1cErrors and sold badges.
--oc-px-accentvar(--oc-primary)The big call-to-action buttons (Start / Confirm / Finish).
--oc-px-accent-textvar(--oc-primary-contrast)Text colour on the call-to-action buttons.
--oc-px-reading-bg#fde047Background of the "we read this" registration / mileage chip.
--oc-px-reading-text#1f2937Text colour of the reading chip.

<oc-showroom> — Showroom image generator

VariableDefaultDescription
--oc-primary#1d4ed8Brand colour — buttons, links, highlights, header bars.
--oc-primary-contrast#ffffffText/icon colour on top of --oc-primary.
--oc-bgtransparentBackground behind the component (transparent = inherit the page).
--oc-surface#ffffffCards, panels and inputs.
--oc-surface-alt#f8fafcSecondary surfaces — transcript area, placeholders, chips.
--oc-border#e2e8f0Borders and dividers.
--oc-text#0f172aBody text.
--oc-muted#64748bSecondary text — hints, specs, counts.
--oc-radius12pxCorner radius of cards and panels.
--oc-radius-sm9pxCorner radius of buttons and inputs.
--oc-fontsystem-ui, 'Segoe UI', Arial, sans-serifFont family for the whole component.
--oc-success#15803dPositive states — in stock, photo accepted, saved.
--oc-warning#b45309Attention states — reserved, AI flagged a photo.
--oc-danger#b91c1cErrors and sold badges.
--oc-showroom-stage-bg#0b0d10The stage behind the backdrop while it loads.

Partner account

GET /v1/me

Requires the partner key only.

Current partner & key scopes

Response 200
{
  "partner": {
    "partner_uid": "00000000-0000-0000-0000-000000000000",
    "name": "string"
  },
  "key": {
    "partner_api_key_uid": "00000000-0000-0000-0000-000000000000",
    "name": "string",
    "prefix": "string",
    "scopes": [
      "string"
    ],
    "expires_at": "string"
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/me" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"

Dealerships

GET /v1/dealerships

Requires the partner key only.

List your dealerships

NameInTypeDescription
offsetqueryinteger
limitqueryinteger
searchquerystring
Response 200
{
  "data": [
    {}
  ],
  "pagination": {
    "offset": 0,
    "limit": 24,
    "total_results": 6
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealerships" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/dealerships

Requires the partner key only.

Create one or more dealerships

The same paste-and-parse flow the Partners portal uses. A dealership always belongs to exactly one region (`GB` or `US`), which drives its currency, tax and units. Domains are the allow-list your publishable partner key may load the components from for this dealership.

Request body
{
  "text": "Example Motors, 12 High Street, Bristol BS1 4AA — sales@example-motors.co.uk — 0117 555 0100 — www.example-motors.co.uk",
  "name": "string",
  "domains": [
    "string"
  ],
  "address": "string",
  "phone": "string",
  "email": "string",
  "country": "GB",
  "region_code": "string",
  "opening_hours": "string",
  "notes": "string",
  "dealerships": [
    {}
  ]
}
Response 201
{
  "data": [
    {
      "dealership_uid": "00000000-0000-0000-0000-000000000000",
      "name": "string",
      "domains": [
        "string"
      ],
      "country": "string",
      "region_code": "string",
      "parsed": {},
      "allowed_origins": [
        "string"
      ],
      "dealership_uid_header": "X-Dealership-Uid",
      "examples": {
        "curl": "curl \"https://api.car-search.ai/v1/dealership/inventory\" \\\n  -H \"Authorization: Bearer csai_live_…\" \\\n  -H \"X-Dealership-Uid: 0d8f3c2a-…\"",
        "snippet": "<script async src=\"https://api.car-search.ai/widgets/loader.js\" data-partner-key=\"csai_pub_…\" data-dealership=\"0d8f3c2a-…\"></script>\n<oc-car-search></oc-car-search>"
      }
    }
  ],
  "count": 0
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealerships" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Example Motors, 12 High Street, Bristol BS1 4AA — sales@example-motors.co.uk — 0117 555 0100 — www.example-motors.co.uk","name":"string","domains":["string"],"address":"string","phone":"string","email":"string","country":"GB","region_code":"string","opening_hours":"string","notes":"string","dealerships":[{}]}'
GET /v1/dealership

Also: /v1/dealerships/{dealership_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get a dealership

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Response 200
{
  "data": {
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "name": "string",
    "slug": "string",
    "region_code": "GB",
    "settings": {},
    "created_at": "string",
    "domains": [
      {
        "dealership_domain_uid": "00000000-0000-0000-0000-000000000000",
        "hostname": "string",
        "is_primary": true,
        "source": "pasted"
      }
    ],
    "components": [
      {
        "widget_uid": "string",
        "tag": "oc-car-search",
        "name": "string",
        "status": "string"
      }
    ],
    "allowed_origins": [
      "string"
    ],
    "dealership_uid_header": "X-Dealership-Uid",
    "examples": {
      "curl": "curl \"https://api.car-search.ai/v1/dealership/inventory\" \\\n  -H \"Authorization: Bearer csai_live_…\" \\\n  -H \"X-Dealership-Uid: 0d8f3c2a-…\"",
      "snippet": "<script async src=\"https://api.car-search.ai/widgets/loader.js\" data-partner-key=\"csai_pub_…\" data-dealership=\"0d8f3c2a-…\"></script>\n<oc-car-search></oc-car-search>"
    }
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
PUT /v1/dealership/domains

Also: /v1/dealerships/{dealership_uid}/domains (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Replace the CORS/allowed-domain list

Replaces the full allow-list. Your publishable partner key is pinned to this list for the dealership, so a change takes effect on the next page load — no key rotation needed.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "domains": [
    "example-motors.co.uk"
  ]
}
Response 200
{
  "data": {
    "domains": [
      {
        "dealership_domain_uid": "string",
        "hostname": "string",
        "is_primary": true,
        "source": "string"
      }
    ]
  }
}
cURL
curl -X PUT "https://api.car-search.ai/v1/dealership/domains" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"domains":["example-motors.co.uk"]}'

Listings

Create, read, update and remove listings — by our `vehicle_uid` or your `external_id`. Every response is the full canonical `Listing`.

GET /v1/dealership/listings

Also: /v1/dealerships/{dealership_uid}/listings (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List listings

Paged, newest-updated first. Every item is the full Listing (with images). Filter by status (comma-separated), published, updated_since (ISO-8601; compares against `updated_at`) and a free-text `q` over make/model/variant/vrm/vin/external_id/stock number. Removed (soft-deleted) listings are never in the default list: ask for them with `status=removed` (alone, or with live statuses: `status=available,removed`). `pagination.total_count` is the count; `total_results` is the same number and deprecated.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
offsetqueryinteger
limitqueryinteger
statusquerystringavailable | reserved | sold | removed, comma-separated for several. `removed` = soft-deleted listings (otherwise never listed).
publishedqueryboolean
updated_sincequerystringOnly listings whose `updated_at` is at or after this instant — poll with your last sync time.
qquerystringFree-text match on make, model, variant, vrm, vin, external_id, stock number.
Response 200
{
  "data": [
    {
      "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
      "external_id": "STK1234",
      "status": "available",
      "published": true,
      "created_at": "2026-09-11T10:15:02.000Z",
      "updated_at": "2026-09-11T10:15:04.000Z",
      "vehicle": {
        "make": "BMW",
        "model": "X5",
        "variant": "xDrive30d M Sport",
        "year": 2021,
        "vrm": "AB12CDE",
        "vin": null,
        "mileage": 32000,
        "mileage_unit": "mi",
        "price": 15995,
        "price_was": null,
        "currency": "GBP",
        "body_type": "SUV",
        "fuel_type": "Diesel",
        "transmission": "Automatic",
        "drivetrain": null,
        "engine_size": "3.0",
        "engine_cc": 3000,
        "engine_litres": 3,
        "doors": 5,
        "seats": null,
        "colour": "Black Sapphire",
        "interior_colour": null,
        "registration_date": null,
        "mot_expiry": null,
        "previous_owners": null,
        "description": "One owner, full BMW service history.",
        "short_description": null,
        "ai_generated_description": false,
        "condition_notes": null,
        "features": [
          "Panoramic roof",
          "Heated seats"
        ],
        "specs": {},
        "stock_number": "STK1234",
        "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
        "video_embed_url": null
      },
      "images": [
        {
          "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
          "position": 0,
          "is_primary": true,
          "category": "primary",
          "shot_type": null,
          "description": null,
          "width": 1920,
          "height": 1280,
          "source_url": "https://example.com/1.jpg",
          "urls": {
            "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
            "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
            "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
          }
        }
      ],
      "pipeline": {
        "status": "received",
        "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
        "received_at": "2026-09-11T10:15:04.000Z",
        "processed_at": null,
        "photos_total": 1,
        "photos_described": 0,
        "enhanced_version": null,
        "enhancing": false,
        "error": null
      },
      "enhanced": null
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/listings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings

Also: /v1/dealerships/{dealership_uid}/listings (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Create a listing

Creates the listing and returns it in full — structured as we hold it, with its `vehicle_uid` — as `201`. Photos (multipart files and/or `images[].url`) are stored before the response, so `images[]` already carries their uids. The listing is vectorized at once (searchable), and the new photos are described by ONE background job that starts a short quiet window after your last write — photos you send in several calls are described together. **No AI copy is written by this call**: when you want the enhanced listing, call `POST …/listings/{listing_id}/enhance` and it is returned. A body field `enhance` is ignored with a warning. Needs a `vrm`, a `vin`, or `make` + `model` — or photos only, in which case the AI reads the plate/VIN and specs off them (see `/listings/from-photos`). A repeated external_id is a `409 conflict` unless `?upsert=1`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
upsertquerybooleanWhen the external_id already exists, update that listing instead of returning 409.
Request body
{
  "external_id": "STK1234",
  "status": "available",
  "vrm": "AB12CDE",
  "make": "BMW",
  "model": "X5",
  "variant": "xDrive30d M Sport",
  "year": 2021,
  "mileage": 32000,
  "mileage_unit": "mi",
  "price": 15995,
  "currency": "GBP",
  "body_type": "SUV",
  "fuel_type": "Diesel",
  "transmission": "Automatic",
  "engine_size": "3.0",
  "doors": 5,
  "colour": "Black Sapphire",
  "description": "One owner, full BMW service history.",
  "features": [
    "Panoramic roof",
    "Heated seats"
  ],
  "images": [
    {
      "url": "https://example.com/1.jpg",
      "position": 1
    }
  ]
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@/path/to/photo.jpg"
POST /v1/dealership/listings/from-photos

Also: /v1/dealerships/{dealership_uid}/listings/from-photos (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Create a listing from photos only

Send photos and nothing else (an optional `external_id`, `price`, `status` or any ListingInput field is welcome). The listing is created immediately and returned as `201` with `pipeline.status: received`; in the background the AI reads the number plate / VIN / odometer off the photos, proposes make, model, year, colour, body, fuel, gearbox, doors and seats, fills the BLANK fields on the listing and vectorizes it — `pipeline.status` becomes `ready` and `GET …/listings/{listing_id}` shows what was identified. It does NOT write the enhanced copy: call `POST …/enhance` when you want it (it returns the enhanced listing), then accept or tweak.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "external_id": "STK1235",
  "price": 12995,
  "images": [
    {
      "url": "https://example.com/plate.jpg"
    },
    {
      "url": "https://example.com/front.jpg"
    }
  ]
}
Response 201
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "auto-3f9a1c2b7d",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": null,
      "model": null,
      "variant": null,
      "year": null,
      "vrm": null,
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": null,
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": null,
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 2,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": [
    "No external_id was supplied; \"auto-3f9a1c2b7d\" was generated — address this listing by its vehicle_uid or that id."
  ]
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/from-photos" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@/path/to/photo.jpg"
GET /v1/dealership/listings/{listing_id}

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get a listing

By vehicle_uid or external_id. The same Listing every other endpoint returns.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "enhanced",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": "2026-09-11T10:16:40.000Z",
      "photos_total": 1,
      "photos_described": 1,
      "enhanced_version": 1,
      "enhancing": false,
      "error": null
    },
    "enhanced": {
      "status": "proposed",
      "version": 1,
      "enhanced_listing_uid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "source": "ai"
    }
  },
  "warnings": []
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/listings/LISTING_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
PUT /v1/dealership/listings/{listing_id}

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Replace a listing (full update)

Full update by vehicle_uid or external_id: send the whole ListingInput. Free-text fields you omit (`description`, `short_description`, `features`) are cleared; identity and numeric columns you omit keep their value. `external_id` may be changed here (409 if taken). Text/spec changes re-vectorize the listing at once; new photos are stored now and described by the background job. Nothing here writes AI copy — call `POST …/enhance` for a fresh enhanced version. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "external_id": "STK1234",
  "status": "available",
  "vrm": "AB12CDE",
  "make": "BMW",
  "model": "X5",
  "variant": "xDrive30d M Sport",
  "year": 2021,
  "mileage": 32000,
  "mileage_unit": "mi",
  "price": 15995,
  "currency": "GBP",
  "body_type": "SUV",
  "fuel_type": "Diesel",
  "transmission": "Automatic",
  "engine_size": "3.0",
  "doors": 5,
  "colour": "Black Sapphire",
  "description": "One owner, full BMW service history.",
  "features": [
    "Panoramic roof",
    "Heated seats"
  ],
  "images": [
    {
      "url": "https://example.com/1.jpg",
      "position": 1
    }
  ]
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X PUT "https://api.car-search.ai/v1/dealership/listings/LISTING_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@/path/to/photo.jpg"
PATCH /v1/dealership/listings/{listing_id}

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Update a listing (partial)

Partial update by vehicle_uid or external_id: send only the fields that changed (any ListingInput field, plus `published`). A price-only change runs nothing; text/spec changes re-vectorize at once; new photos (`images[].url` or multipart) are stored now and described by the background job. Nothing here writes AI copy — call `POST …/enhance` for a fresh enhanced version. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "price": 14995,
  "price_was": 15995
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 14995,
      "price_was": 15995,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X PATCH "https://api.car-search.ai/v1/dealership/listings/LISTING_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@/path/to/photo.jpg"
DELETE /v1/dealership/listings/{listing_id}

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Remove a listing

Soft-deletes the listing: it leaves the default list, search, inventory and the website at once; history is kept. Returns the Listing with `status: removed`. To confirm or audit the delete, `GET …/listings/{listing_id}` still returns it (with `status: removed`) and `GET …/listings?status=removed` lists the removed ones. To mark a car sold or reserved instead, use `POST …/status`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "removed",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X DELETE "https://api.car-search.ai/v1/dealership/listings/LISTING_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings/{listing_id}/status

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/status (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Set the sale status

`available`, `reserved` or `sold` (sold stamps the sold date). Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "status": "sold"
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "sold",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/status" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"status":"sold"}'
POST /v1/dealership/listings/{listing_id}/publish

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/publish (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Publish a listing

Makes the listing visible on the dealership website and public surfaces (`published: true`). Independent of the sale status. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/publish" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings/{listing_id}/unpublish

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/unpublish (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Unpublish a listing

Hides the listing from the website and public surfaces without removing it (`published: false`). Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": false,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/unpublish" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

Images

A listing's photos: add (files or URLs), reorder, choose the primary, remove. Each change returns the full Listing.

GET /v1/dealership/listings/{listing_id}/images

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/images (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List a listing's photos

The `images[]` array of the Listing, in display order.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": [
    {
      "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
      "position": 0,
      "is_primary": true,
      "category": "primary",
      "shot_type": null,
      "description": null,
      "width": 1920,
      "height": 1280,
      "source_url": "https://example.com/1.jpg",
      "urls": {
        "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
        "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
        "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
      }
    }
  ]
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/images" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings/{listing_id}/images

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/images (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Add photos

Multipart `images` files and/or `{ urls: [] }`. Stored before the response (so the new uids are in `images[]`), appended after the existing photos, then described by the AI in ONE background job per listing that starts a short quiet window after your last call (send 30 photos in 30 calls and they are described together; `pipeline.ingest_uid` stays the same). Each photo is described once, ever. No AI copy is written — call `POST …/enhance` for that. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "urls": [
    "https://example.com/2.jpg",
    "https://example.com/3.jpg"
  ]
}
Response 201
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/images" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "images=@/path/to/photo.jpg"
PUT /v1/dealership/listings/{listing_id}/images/order

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/images/order (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Reorder photos

Send the image uids in the order you want. Photos you leave out keep their relative order after the ones named. The first becomes the primary photo. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "image_uids": [
    "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
    "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
  ]
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X PUT "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/images/order" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"image_uids":["6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01","b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"]}'
DELETE /v1/dealership/listings/{listing_id}/images/{image_uid}

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/images/{image_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Remove a photo

Removes the photo from the listing; the remaining photos are renumbered and the next one becomes primary if needed. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
image_uid *pathstringAn `image_uid` from the listing's `images[]`.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X DELETE "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/images/IMAGE_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings/{listing_id}/images/{image_uid}/primary

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/images/{image_uid}/primary (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Make a photo the primary

Moves the photo to position 0 (the card / hero image); the others keep their relative order. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
image_uid *pathstringAn `image_uid` from the listing's `images[]`.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "received",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": null,
      "photos_total": 1,
      "photos_described": 0,
      "enhanced_version": null,
      "enhancing": false,
      "error": null
    },
    "enhanced": null
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/images/IMAGE_UID/primary" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

Enhanced listings

The AI-written version of a listing (from the original plus every photo description). It is produced ONLY by `POST …/enhance`, which returns it; then read it, accept it, or send back your tweaked version.

GET /v1/dealership/listings/{listing_id}/enhanced

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/enhanced (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get the AI-enhanced listing

The current enhanced version (AI-proposed, accepted, or your tweaked one): headline, description, highlights, specs, features, condition notes, every photo description, `original` (the listing's vehicle block as it stands) and the `pipeline` block. It exists only after `POST …/enhance` has run: until then `404 not_generated` (the body carries `pipeline`); `404 not_ready` while an enhance that answered `202` is still finishing; `409 pipeline_failed` if the last AI pass failed — call `POST …/enhance` again.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "enhanced_listing_uid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "version": 1,
    "status": "proposed",
    "source": "ai",
    "headline": "Immaculate BMW X5 xDrive30d M Sport — panoramic roof, one owner",
    "description": "This 2021 BMW X5 xDrive30d M Sport …",
    "highlights": [
      "One owner from new",
      "Panoramic roof",
      "Heated seats"
    ],
    "specs": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "body_type": "SUV",
      "exterior_color": "Black Sapphire",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "doors": 5,
      "year": 2021,
      "mileage": 32000
    },
    "features": [
      "Panoramic roof",
      "Heated seats",
      "Harman Kardon audio"
    ],
    "condition_notes": null,
    "image_descriptions": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "shot_type": "front_three_quarter",
        "description": "Front three-quarter view of a black BMW X5 …",
        "search_description": null,
        "tags": [
          "M Sport body kit",
          "20-inch alloys"
        ]
      }
    ],
    "model_id": "claude-…",
    "cost_usd": 0.0412,
    "created_at": "2026-09-11T10:16:40.000Z",
    "original": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    }
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/enhanced" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
PUT /v1/dealership/listings/{listing_id}/enhanced

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/enhanced (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Send back a tweaked listing

Your edited version of the AI proposal. Fields you omit are taken from the current version; the result is stored as a new version (`source: tweaked`, `status: accepted`) and applied to the live listing (copy → description, specs/features → the vehicle), then re-vectorized. Returns the full Listing.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "headline": "One-owner BMW X5 M Sport with panoramic roof",
  "features": [
    "Panoramic roof",
    "Heated seats",
    "Harman Kardon audio",
    "Tow bar"
  ]
}
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "This 2021 BMW X5 xDrive30d M Sport …",
      "short_description": "One-owner BMW X5 M Sport with panoramic roof",
      "ai_generated_description": true,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "accepted",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": "2026-09-11T10:16:40.000Z",
      "photos_total": 1,
      "photos_described": 1,
      "enhanced_version": 1,
      "enhancing": false,
      "error": null
    },
    "enhanced": {
      "status": "accepted",
      "version": 2,
      "enhanced_listing_uid": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
      "source": "tweaked"
    }
  },
  "warnings": []
}
cURL
curl -X PUT "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/enhanced" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"headline":"One-owner BMW X5 M Sport with panoramic roof","features":["Panoramic roof","Heated seats","Harman Kardon audio","Tow bar"]}'
POST /v1/dealership/listings/{listing_id}/enhanced/accept

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/enhanced/accept (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Accept the AI-enhanced listing

Applies the proposed version as-is: headline → short description, copy → description, specs and features → the vehicle; marks it accepted and re-vectorizes. Returns the full Listing (`enhanced.status: accepted`).

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Response 200
{
  "data": {
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "status": "available",
    "published": true,
    "created_at": "2026-09-11T10:15:02.000Z",
    "updated_at": "2026-09-11T10:15:04.000Z",
    "vehicle": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "This 2021 BMW X5 xDrive30d M Sport …",
      "short_description": "Immaculate BMW X5 xDrive30d M Sport — panoramic roof, one owner",
      "ai_generated_description": true,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats",
        "Harman Kardon audio"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "images": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "position": 0,
        "is_primary": true,
        "category": "primary",
        "shot_type": null,
        "description": null,
        "width": 1920,
        "height": 1280,
        "source_url": "https://example.com/1.jpg",
        "urls": {
          "thumb": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=thumb&dealership_uid=DEALERSHIP_UID",
          "medium": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=medium&dealership_uid=DEALERSHIP_UID",
          "full": "https://api.car-search.ai/v1/files/6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01?size=full&dealership_uid=DEALERSHIP_UID"
        }
      }
    ],
    "pipeline": {
      "status": "accepted",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": "2026-09-11T10:16:40.000Z",
      "photos_total": 1,
      "photos_described": 1,
      "enhanced_version": 1,
      "enhancing": false,
      "error": null
    },
    "enhanced": {
      "status": "accepted",
      "version": 1,
      "enhanced_listing_uid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "source": "ai"
    }
  },
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/enhanced/accept" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/listings/{listing_id}/enhance

Also: /v1/dealerships/{dealership_uid}/listings/{listing_id}/enhance (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Generate the enhanced listing (the only call that writes AI copy)

THE enhancement pass, run synchronously: any photo not yet described is described (in parallel), specs are extracted from every photo description, the AI writes headline, description, highlights, specs, features and condition notes, the listing is re-vectorized, a new proposed version is stored and **returned in this response** (`200`, the same object as `GET …/enhanced` plus `pipeline`). Nothing else on this API writes an enhanced version — pushing a listing or photos never does. Calling it again when nothing changed (no photo added or removed, no text or spec edited) returns the current version — proposed, accepted or tweaked — with `unchanged: true` and costs nothing. Pass `{ "identify": true }` to force a fresh pass that also re-reads plate / VIN / specs off the photos into the blank fields. A normal listing (up to ~30 photos) finishes well inside one request; only when the photos cannot all be described inside the request budget does it answer `202` with the full Listing (`pipeline.status: processing`, `pipeline.enhancing: true`) and finish in the background — poll `GET …/enhanced` or take the `listing.enhanced.ready` webhook, which fires in both cases.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
listing_id *pathstringEITHER the listing's `vehicle_uid` (GUID) OR your own `external_id` — both address the same listing.
Request body
{
  "identify": false
}
Response 200
{
  "data": {
    "enhanced_listing_uid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    "vehicle_uid": "0d8f3c2a-7e41-4b6f-9a2d-5c1e8b7f6a30",
    "external_id": "STK1234",
    "version": 1,
    "status": "proposed",
    "source": "ai",
    "headline": "Immaculate BMW X5 xDrive30d M Sport — panoramic roof, one owner",
    "description": "This 2021 BMW X5 xDrive30d M Sport …",
    "highlights": [
      "One owner from new",
      "Panoramic roof",
      "Heated seats"
    ],
    "specs": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "body_type": "SUV",
      "exterior_color": "Black Sapphire",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "doors": 5,
      "year": 2021,
      "mileage": 32000
    },
    "features": [
      "Panoramic roof",
      "Heated seats",
      "Harman Kardon audio"
    ],
    "condition_notes": null,
    "image_descriptions": [
      {
        "image_uid": "6f1c2a9e-4b1d-4c0e-9d4e-2f3a7b8c9d01",
        "shot_type": "front_three_quarter",
        "description": "Front three-quarter view of a black BMW X5 …",
        "search_description": null,
        "tags": [
          "M Sport body kit",
          "20-inch alloys"
        ]
      }
    ],
    "model_id": "claude-…",
    "cost_usd": 0.0412,
    "created_at": "2026-09-11T10:16:40.000Z",
    "original": {
      "make": "BMW",
      "model": "X5",
      "variant": "xDrive30d M Sport",
      "year": 2021,
      "vrm": "AB12CDE",
      "vin": null,
      "mileage": 32000,
      "mileage_unit": "mi",
      "price": 15995,
      "price_was": null,
      "currency": "GBP",
      "body_type": "SUV",
      "fuel_type": "Diesel",
      "transmission": "Automatic",
      "drivetrain": null,
      "engine_size": "3.0",
      "engine_cc": 3000,
      "engine_litres": 3,
      "doors": 5,
      "seats": null,
      "colour": "Black Sapphire",
      "interior_colour": null,
      "registration_date": null,
      "mot_expiry": null,
      "previous_owners": null,
      "description": "One owner, full BMW service history.",
      "short_description": null,
      "ai_generated_description": false,
      "condition_notes": null,
      "features": [
        "Panoramic roof",
        "Heated seats"
      ],
      "specs": {},
      "stock_number": "STK1234",
      "url_slug": "bmw-x5-xdrive30d-m-sport-ab12cde",
      "video_embed_url": null
    },
    "pipeline": {
      "status": "enhanced",
      "ingest_uid": "3c2b1a09-8f7e-4d6c-b5a4-3f2e1d0c9b8a",
      "received_at": "2026-09-11T10:15:04.000Z",
      "processed_at": "2026-09-11T10:16:40.000Z",
      "photos_total": 1,
      "photos_described": 1,
      "enhanced_version": 1,
      "enhancing": false,
      "error": null
    }
  },
  "unchanged": false,
  "warnings": []
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/listings/LISTING_ID/enhance" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"identify":false}'

Inventory

GET /v1/dealership/inventory

Also: /v1/dealerships/{dealership_uid}/inventory (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List current inventory

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
offsetqueryinteger
limitqueryinteger
qquerystringFree-text filter.
stock_statusquerystring (active | reserved | sold | sor | coming_soon)
publishedquerystring (0 | 1)Pass `0` to include unpublished stock.
include_soldquerystring (0 | 1)
Response 200
{
  "data": [
    {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 24,
    "total_results": 6
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/inventory" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/inventory/{vehicle_uid}

Also: /v1/dealerships/{dealership_uid}/inventory/{vehicle_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get one vehicle

The path parameter accepts either the vehicle UID or its public URL slug.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
vehicle_uid *pathstring
Response 200
{
  "data": {
    "vehicle": {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    },
    "images": [
      {
        "image_uid": "00000000-0000-0000-0000-000000000000",
        "image_category": "string",
        "display_order": 0,
        "description": "string",
        "shot_type": "string",
        "shot_confidence": 0,
        "width": 0,
        "height": 0,
        "source_url": "string",
        "url": "/v1/files/00000000-0000-0000-0000-000000000000"
      }
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/inventory/VEHICLE_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

Search & chat

POST /v1/dealership/search

Also: /v1/dealerships/{dealership_uid}/search (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Semantic inventory search

The AI interprets shopper intent from free text ("economical family SUV under 20k") into hard filters plus a ranked semantic match over both listing text and image descriptions, and tells you what it understood via `interpretation`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "query": "economical family SUV under 20k",
  "filters": {
    "make": "Kia"
  }
}
Response 200
{
  "data": [
    {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "mode": "semantic",
  "photo_matches": 0,
  "interpretation": {
    "summary": "string",
    "filters": {},
    "semantic_query": "string"
  },
  "pagination": {
    "offset": 0,
    "limit": 24,
    "total_results": 6
  },
  "timing_ms": 0
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/search" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"query":"economical family SUV under 20k","filters":{"make":"Kia"}}'
POST /v1/dealership/chat

Also: /v1/dealerships/{dealership_uid}/chat (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

One chat turn with the AI sales assistant — server manages the conversation context

Send the shopper's new message (plus `conversation_uid` after the first call); the server rebuilds the full model context from the stored conversation itself — you never assemble or resend history. The assistant can search live stock and answer buyer questions. Use `/chat/stream` instead for Server-Sent Events with live "thinking/searching" status updates.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "message": "string",
  "conversation_uid": "string",
  "visitor_id": "string",
  "vehicle_uid": "00000000-0000-0000-0000-000000000000",
  "history": [
    {}
  ]
}
Response 200
{
  "data": {
    "conversation_uid": "9c2e4b7a-1234-4a6b-9c3d-abcdef012345",
    "message": "We have a couple of great family SUVs around that budget — the 2021 Kia Sportage and the 2020 Nissan Qashqai both stand out for space and efficiency.",
    "suggestions": [
      "Tell me about the Sportage",
      "Anything cheaper?",
      "Book a viewing"
    ],
    "vehicles": [
      {
        "vehicle_uid": "11111111-2222-3333-4444-555555555555",
        "make": "Kia",
        "model": "Sportage",
        "year": 2021,
        "price": 19995,
        "currency": "GBP",
        "mileage": 18500,
        "fuel": "Petrol",
        "transmission": "Automatic",
        "image_url": "/api/v1/images/…/medium"
      }
    ],
    "tool_calls": [
      {
        "name": "search_inventory",
        "label": "Searching stock…",
        "status": "done",
        "duration_ms": 380,
        "result_count": 2
      }
    ]
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/chat" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"message":"string","conversation_uid":"string","visitor_id":"string","vehicle_uid":"00000000-0000-0000-0000-000000000000","history":[{}]}'
POST /v1/dealership/chat/stream

Also: /v1/dealerships/{dealership_uid}/chat/stream (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Chat turn, streamed as Server-Sent Events with live tool-call status

Same body and server-managed context as `/chat`. Response is `text/event-stream`; each `data:` line is one JSON event, in this order: one `meta`, then any mix of `status` (a tool call starting, or the coarse thinking/writing phase — use it to flip on a "Searching…" indicator), `tool` (that tool call finishing — flip the indicator off), and `delta` (a chunk of the reply's text), ending in one terminal `done` carrying the full reply. A mid-stream failure sends `error` instead of `done`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "message": "string",
  "conversation_uid": "string",
  "visitor_id": "string",
  "vehicle_uid": "00000000-0000-0000-0000-000000000000",
  "history": [
    {}
  ]
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/chat/stream" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"message":"string","conversation_uid":"string","visitor_id":"string","vehicle_uid":"00000000-0000-0000-0000-000000000000","history":[{}]}'

Conversations

Chatbot conversation history and AI summaries for a dealership — what shoppers asked, which vehicles they looked at, whether contact details were captured.

GET /v1/dealership/conversations

Also: /v1/dealerships/{dealership_uid}/conversations (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List a dealership's chatbot conversations

One row per website-visitor chat session with the AI assistant. Filter by `visitor_id` (the widget's per-browser id) or `since` (ISO date/time); newest first.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
visitor_idquerystringThe widget's anonymous per-browser visitor id.
sincequerystringOnly conversations started at or after this ISO timestamp.
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "conversation_uid": "9e5b7e2a-1e0a-4a3a-9b0a-1a2b3c4d5e6f",
      "visitor_id": "v_9f8a7b6c5d4e",
      "channel": "widget",
      "started_at": "2026-09-10T14:02:11Z",
      "last_message_at": "2026-09-10T14:05:47Z",
      "message_count": 6,
      "cost_usd": 0.0042,
      "has_lead": true,
      "has_summary": true,
      "intent": {
        "stage": "ready_to_buy",
        "score": 82
      }
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_results": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/conversations" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/conversations/{conversation_uid}

Also: /v1/dealerships/{dealership_uid}/conversations/{conversation_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get the complete history of one conversation

Ordered messages (role, text, timestamp). Assistant replies are HTML in storage — stripped to plain text unless `?format=html`. Tool calls (inventory searches, vehicle lookups, lead capture, …) are summarised with the vehicle ids they touched rather than their raw payload.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
conversation_uid *pathstringThe conversation UID (returned by `POST /v1/dealerships/{uid}/chat` as `conversation_uid`, or listed by `GET /v1/dealerships/{uid}/conversations`).
formatquerystring (text | html)Pass `html` to get assistant replies as the original HTML instead of stripped plain text.
Response 200
{
  "data": {
    "conversation_uid": "9e5b7e2a-1e0a-4a3a-9b0a-1a2b3c4d5e6f",
    "visitor_id": "v_9f8a7b6c5d4e",
    "channel": "widget",
    "started_at": "2026-09-10T14:02:11Z",
    "ended_at": null,
    "last_message_at": "2026-09-10T14:05:47Z",
    "message_count": 4,
    "cost_usd": 0.0042,
    "intent": {
      "stage": "ready_to_buy",
      "score": 82
    },
    "lead": {
      "captured": true,
      "name": "Sam Carter",
      "email": "sam@example.com",
      "phone": "07700900123"
    },
    "messages": [
      {
        "role": "user",
        "text": "Do you have any automatic diesel SUVs under 30k?",
        "created_at": "2026-09-10T14:02:11Z"
      },
      {
        "role": "tool",
        "tool": "search_inventory",
        "vehicle_ids": [
          "3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c"
        ],
        "created_at": "2026-09-10T14:02:12Z"
      },
      {
        "role": "assistant",
        "text": "We have a 2021 BMW X3 xDrive20d in stock for £28,995 — automatic, diesel, 34,000 miles. Would you like more details?",
        "created_at": "2026-09-10T14:02:13Z"
      },
      {
        "role": "user",
        "text": "Yes please, and I'm Sam, sam@example.com, 07700900123",
        "created_at": "2026-09-10T14:05:47Z"
      }
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/conversations/CONVERSATION_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/conversations/{conversation_uid}/summary

Also: /v1/dealerships/{dealership_uid}/conversations/{conversation_uid}/summary (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get (or generate) an AI summary of a conversation

Reuses the same summarisation the DMS uses for its own conversation summaries (`summarizeConversation`, a cheap model with a sales-analyst prompt), parsed into a structured shape. Cached on the conversation and only regenerated when new messages have arrived since the last summary — pass `?refresh=1` to force it. Counts against the dealership's AI daily cost cap.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
conversation_uid *pathstringThe conversation UID (returned by `POST /v1/dealerships/{uid}/chat` as `conversation_uid`, or listed by `GET /v1/dealerships/{uid}/conversations`).
refreshquerystring (0 | 1)Pass `1` to regenerate even if a fresh cached summary exists.
Response 200
{
  "data": {
    "summary": "Vehicles: 2021 BMW X3 xDrive20d, £28,995.\nNeeds: automatic diesel SUV under £30k.\nIntent: high — asked for full details and shared contact info unprompted.\nNext step: call Sam today to confirm the X3 is still available and offer a test drive.\nLead quality: Hot — ready to buy, contact captured in-chat.",
    "intent": "high — asked for full details and shared contact info unprompted.",
    "vehicles_of_interest": [
      "3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c"
    ],
    "contact_captured": true,
    "next_best_action": "call Sam today to confirm the X3 is still available and offer a test drive.",
    "generated_at": "2026-09-10T14:06:02Z"
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/conversations/CONVERSATION_UID/summary" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

AI

What the assistant costs on each of your dealerships, and how each one behaves. Set the cost per conversation, the daily cost cap and the custom instructions injected into that dealership's embedded chat; read the spend back conversation by conversation.

GET /v1/dealerships/{dealership_uid}/ai/settings

Read a dealership's AI settings

The stored values, what they work out to (`effective`), and a machine-readable description of every writable field with its range and its blank-value behaviour (`fields`) — enough to build a settings screen without hard-coding anything.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Response 200
{
  "data": {
    "dealership_uid": "2e78b4d8-3724-46af-9638-f1b7221728a6",
    "settings": {
      "assistant_name": "Ava",
      "welcome_message": null,
      "system_instructions": "Always mention our seven day money back promise. Never quote a finance rate.",
      "model_id": "claude-haiku-4-5",
      "temperature": null,
      "max_tokens": 4096,
      "session_cost_cap_usd": 0.02,
      "daily_cost_cap_usd": 5,
      "session_msg_limit": 50,
      "widget_enabled": true
    },
    "effective": {
      "model_id": "claude-haiku-4-5",
      "assistant_name": "Ava",
      "max_tokens": 4096,
      "temperature": null,
      "session_cost_cap_usd": 0.02,
      "session_cost_cap_applies": true,
      "daily_cost_cap_usd": 5,
      "daily_cost_cap_applies": true,
      "session_msg_limit": 50,
      "session_msg_limit_applies": true,
      "widget_enabled": true,
      "currency": "USD"
    }
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealerships/DEALERSHIP_UID/ai/settings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
PUT /v1/dealerships/{dealership_uid}/ai/settings

Change a dealership's AI settings

A partial write: only the fields you send are touched, the rest keep their value. Sending `null` on a field that allows it puts that field back to the standard behaviour, so `{"session_cost_cap_usd": null}` returns to $0.50 a conversation while `{"session_cost_cap_usd": 0}` means no cap at all. Values are range-checked the same way the dealership's own settings screen checks them; a value out of range is a 400 that names the field, and nothing is written.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "session_cost_cap_usd": 0.02,
  "daily_cost_cap_usd": 5,
  "system_instructions": "Always mention our seven day money back promise. Never quote a finance rate.",
  "assistant_name": "Ava"
}
Response 200
{
  "data": {
    "dealership_uid": "2e78b4d8-3724-46af-9638-f1b7221728a6",
    "settings": {
      "assistant_name": "Ava",
      "welcome_message": null,
      "system_instructions": "Always mention our seven day money back promise. Never quote a finance rate.",
      "model_id": "claude-haiku-4-5",
      "temperature": null,
      "max_tokens": 4096,
      "session_cost_cap_usd": 0.02,
      "daily_cost_cap_usd": 5,
      "session_msg_limit": 50,
      "widget_enabled": true
    },
    "effective": {
      "model_id": "claude-haiku-4-5",
      "assistant_name": "Ava",
      "max_tokens": 4096,
      "temperature": null,
      "session_cost_cap_usd": 0.02,
      "session_cost_cap_applies": true,
      "daily_cost_cap_usd": 5,
      "daily_cost_cap_applies": true,
      "session_msg_limit": 50,
      "session_msg_limit_applies": true,
      "widget_enabled": true,
      "currency": "USD"
    },
    "updated": [
      "session_cost_cap_usd",
      "daily_cost_cap_usd",
      "system_instructions",
      "assistant_name"
    ]
  }
}
cURL
curl -X PUT "https://api.car-search.ai/v1/dealerships/DEALERSHIP_UID/ai/settings" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"session_cost_cap_usd":0.02,"daily_cost_cap_usd":5,"system_instructions":"Always mention our seven day money back promise. Never quote a finance rate.","assistant_name":"Ava"}'
GET /v1/dealerships/{dealership_uid}/ai/usage

One dealership's AI spend, conversation by conversation

Totals for the window, a figure for each day, and the individual conversations with what each one cost and whether the cost cap stopped it. Money is US dollars, the currency the models are billed in; nothing is converted.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
fromquerystringFirst day of the window, `YYYY-MM-DD` UTC. Send `from` and `to` together.
toquerystringLast day of the window, inclusive.
daysqueryintegerInstead of `from`/`to`: the last N days up to today. Defaults to 30.
offsetqueryintegerPaging over the conversations.
limitqueryinteger
Response 200
{
  "data": {
    "dealership_uid": "2e78b4d8-3724-46af-9638-f1b7221728a6",
    "totals": {
      "from": "2026-09-06",
      "to": "2026-09-12",
      "currency": "USD",
      "conversations": 34,
      "messages": 291,
      "input_tokens": 184032,
      "output_tokens": 41120,
      "cost_usd": 0.7412,
      "avg_cost_per_conversation_usd": 0.0218,
      "conversation_cap_hits": 3,
      "daily_cap_days": 0,
      "message_cap_hits": 0
    },
    "days": [
      {
        "date": "2026-09-11",
        "cost_usd": 0.1284,
        "conversations": 6,
        "messages": 51
      },
      {
        "date": "2026-09-12",
        "cost_usd": 0.0932,
        "conversations": 4,
        "messages": 38
      }
    ],
    "conversations": [
      {
        "conversation_uid": "7a1f7d1e-0f3c-4f21-9a1e-2b64d3f0c911",
        "visitor_id": "web-9f2a",
        "channel": "widget",
        "model_id": "claude-haiku-4-5",
        "started_at": "2026-09-12T09:14:02Z",
        "last_message_at": "2026-09-12T09:21:44Z",
        "messages": 16,
        "input_tokens": 5332,
        "output_tokens": 1210,
        "cost_usd": 0.0211,
        "stopped_by_conversation_cap": true,
        "stopped_by_message_limit": false,
        "has_lead": true
      }
    ]
  },
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 34
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealerships/DEALERSHIP_UID/ai/usage" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/ai/usage

Requires the partner key only.

AI spend across every dealership you support

The rollup behind the partner portal's AI page: totals for the window, one row per dealership with its cost, its average per conversation and how often its caps bit, and a figure for each day across all of them. No dealership uid and no `X-Dealership-Uid` header — this one is partner-level.

NameInTypeDescription
fromquerystringFirst day of the window, `YYYY-MM-DD` UTC. Send `from` and `to` together.
toquerystringLast day of the window, inclusive.
daysqueryintegerInstead of `from`/`to`: the last N days up to today. Defaults to 30.
Response 200
{
  "data": {
    "totals": {
      "from": "2026-08-14",
      "to": "2026-09-12",
      "currency": "USD",
      "conversations": 412,
      "messages": 3688,
      "input_tokens": 2214880,
      "output_tokens": 501233,
      "cost_usd": 9.4187,
      "avg_cost_per_conversation_usd": 0.0229,
      "conversation_cap_hits": 14,
      "daily_cap_days": 1,
      "message_cap_hits": 0
    },
    "dealerships": [
      {
        "dealership_uid": "2e78b4d8-3724-46af-9638-f1b7221728a6",
        "name": "Aylesbury AI Motors",
        "conversations": 210,
        "messages": 1904,
        "input_tokens": 1120441,
        "output_tokens": 254008,
        "cost_usd": 5.2011,
        "avg_cost_per_conversation_usd": 0.0248,
        "conversation_cap_hits": 11,
        "daily_cap_days": 1,
        "session_cost_cap_usd": 0.02,
        "daily_cost_cap_usd": 5,
        "widget_enabled": true
      },
      {
        "dealership_uid": "a64ec039-1dea-4bc9-b1a9-f8dee610bb0d",
        "name": "Bicester AI Cars",
        "conversations": 202,
        "messages": 1784,
        "input_tokens": 1094439,
        "output_tokens": 247225,
        "cost_usd": 4.2176,
        "avg_cost_per_conversation_usd": 0.0209,
        "conversation_cap_hits": 3,
        "daily_cap_days": 0,
        "session_cost_cap_usd": null,
        "daily_cost_cap_usd": null,
        "widget_enabled": true
      }
    ],
    "days": [
      {
        "date": "2026-09-12",
        "cost_usd": 0.3411,
        "conversations": 15,
        "messages": 132
      }
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/ai/usage" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"

CRM

Everything the AI captured — leads, contacts, part-exchanges, appointments — is yours through the API; the same records the dealership sees. A lead is the dealership's own enquiry, so a stage you set here is the stage its sales team sees.

GET /v1/dealership/crm/leads

Also: /v1/dealerships/{dealership_uid}/crm/leads (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List the dealership's leads

Every lead in the dealership CRM, newest change first — the ones the AI chat captured, the part-exchanges, the website forms and the ones you pushed. Filter by status, stage, source, contact, free text, or everything changed since a timestamp (the polling pattern: keep the newest `updated_at` you saw and pass it back as `updated_since`).

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
statusquerystring (open | won | lost)
stage_uidquerystringA stage uid from `GET /crm/pipeline`.
sourcequerystring (ai_chat | part_exchange | form | api | semantic_search | other)Where the lead came from. `ai_chat` = captured by the AI assistant in conversation.
contact_uidquerystringOnly this contact's leads.
updated_sincequerystringOnly leads changed at or after this ISO timestamp.
qquerystringFree text over the contact (name, company, email, phone) and the vehicle (make, model, reg).
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "lead_uid": "7f2b1d84-3c55-4f6e-9a11-2b8e0c9d4a01",
      "dealership_uid": "DEALERSHIP_UID",
      "status": "open",
      "stage": {
        "stage_uid": "b1c2d3e4-0000-4000-8000-000000000002",
        "name": "Contacted",
        "sort_order": 2,
        "is_terminal": false,
        "is_won": false
      },
      "source": {
        "type": "ai_chat",
        "raw": "ai_engagement",
        "conversation_uid": "9c2e4b7a-1234-4a6b-9c3d-abcdef012345",
        "conversation_url": "/v1/dealerships/DEALERSHIP_UID/conversations/9c2e4b7a-1234-4a6b-9c3d-abcdef012345",
        "part_exchange_uid": null,
        "engagement_lead_uid": "55aa11bb-2233-4455-6677-8899aabbccdd"
      },
      "contact": {
        "contact_uid": "3a9f0e12-77bb-4c31-9d55-0f1e2d3c4b5a",
        "first_name": "Casey",
        "last_name": "Brown",
        "name": "Casey Brown",
        "company_name": null,
        "email": "casey@example.com",
        "phone": "07700 900123",
        "mobile": null,
        "consent": {
          "do_not_email": false,
          "do_not_call": false,
          "do_not_text": false,
          "marketing_opt_in": true
        },
        "url": "/v1/dealerships/DEALERSHIP_UID/crm/contacts/3a9f0e12-77bb-4c31-9d55-0f1e2d3c4b5a"
      },
      "vehicle_of_interest": {
        "vehicle_uid": "11111111-2222-3333-4444-555555555555",
        "make": "Kia",
        "model": "Sportage",
        "variant": "1.6T GT-Line",
        "year": 2021,
        "vrm": "AB21CDE",
        "vin": null
      },
      "part_exchange": null,
      "ai": {
        "summary": "Family shopper looking for a 5-seat SUV under £22k, part-exchanging a 2015 Focus. Asked about service history and finance. Ready to view this weekend.",
        "summary_at": "2026-09-11T14:03:22Z",
        "intent_score": 78,
        "intent_stage": "ready",
        "notes": "Wants a weekend viewing; two children, needs boot space.",
        "looking_for": {
          "make": "Kia",
          "model": null,
          "body_type": "SUV",
          "fuel_type": null,
          "budget_min": null,
          "budget_max": 22000,
          "timeframe": "this month"
        },
        "conversation": {
          "conversation_uid": "9c2e4b7a-1234-4a6b-9c3d-abcdef012345",
          "visitor_id": "v-8f3a…",
          "started_at": "2026-09-11T13:58:10Z",
          "message_count": 11
        },
        "interested_vehicle_uids": [
          "11111111-2222-3333-4444-555555555555"
        ]
      },
      "enquiry_type": "sales",
      "budget": null,
      "target_price": null,
      "target_monthly": null,
      "assignee_uid": null,
      "appointment_at": "2026-09-14T10:30:00Z",
      "next_action": {
        "at": null,
        "note": "Call to confirm the viewing"
      },
      "lost_reason": null,
      "converted_vehicle_uid": null,
      "created_at": "2026-09-11T14:03:20Z",
      "updated_at": "2026-09-11T15:10:02Z",
      "url": "/v1/dealerships/DEALERSHIP_UID/crm/leads/7f2b1d84-3c55-4f6e-9a11-2b8e0c9d4a01"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/leads" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/crm/leads

Also: /v1/dealerships/{dealership_uid}/crm/leads (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Create a lead

Puts a lead into the dealership CRM exactly as a website form or the AI assistant would: it finds or creates the contact by email, then opens an enquiry at the first pipeline stage. Its `source.type` is `api`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "contact": {
    "name": "Pat Partner",
    "email": "pat@example.com",
    "phone": "07700 900456"
  },
  "message": "Wants a diesel estate under £20k."
}
Response 201
{
  "data": {
    "lead_uid": "00000000-0000-0000-0000-000000000000",
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "status": "open",
    "stage": null,
    "source": {
      "type": "ai_chat",
      "raw": "string",
      "conversation_uid": "00000000-0000-0000-0000-000000000000",
      "conversation_url": "string",
      "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
      "engagement_lead_uid": "00000000-0000-0000-0000-000000000000"
    },
    "contact": {
      "contact_uid": "00000000-0000-0000-0000-000000000000",
      "first_name": "string",
      "last_name": "string",
      "name": "string",
      "company_name": "string",
      "email": "string",
      "phone": "string",
      "mobile": "string",
      "consent": {
        "do_not_email": true,
        "do_not_call": true,
        "do_not_text": true,
        "marketing_opt_in": true
      },
      "url": "string"
    },
    "vehicle_of_interest": {},
    "part_exchange": {},
    "ai": {
      "summary": "string",
      "summary_at": "string",
      "intent_score": 0,
      "intent_stage": "considering",
      "notes": "string",
      "looking_for": {},
      "conversation": {},
      "interested_vehicle_uids": [
        "00000000-0000-0000-0000-000000000000"
      ]
    },
    "enquiry_type": "string",
    "budget": 0,
    "target_price": 0,
    "target_monthly": 0,
    "assignee_uid": "00000000-0000-0000-0000-000000000000",
    "appointment_at": "string",
    "next_action": {
      "at": "string",
      "note": "string"
    },
    "lost_reason": "string",
    "converted_vehicle_uid": "00000000-0000-0000-0000-000000000000",
    "created_at": "string",
    "updated_at": "string",
    "url": "string",
    "stage_history": [
      {}
    ],
    "activities": [
      {}
    ],
    "notes": [
      {}
    ],
    "appointments": [
      {}
    ],
    "tasks": [
      {}
    ]
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/crm/leads" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"contact":{"name":"Pat Partner","email":"pat@example.com","phone":"07700 900456"},"message":"Wants a diesel estate under £20k."}'
GET /v1/dealership/crm/leads/{lead_uid}

Also: /v1/dealerships/{dealership_uid}/crm/leads/{lead_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

One lead, in full

The whole record: contact, the car they want, the part-exchange they offered, the AI summary and intent score, the originating conversation (with a link to the transcript), the stage history, the timeline, the notes, the appointments and the follow-up tasks.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
lead_uid *pathstring
Response 200
{
  "data": {
    "lead_uid": "00000000-0000-0000-0000-000000000000",
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "status": "open",
    "stage": null,
    "source": {
      "type": "ai_chat",
      "raw": "string",
      "conversation_uid": "00000000-0000-0000-0000-000000000000",
      "conversation_url": "string",
      "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
      "engagement_lead_uid": "00000000-0000-0000-0000-000000000000"
    },
    "contact": {
      "contact_uid": "00000000-0000-0000-0000-000000000000",
      "first_name": "string",
      "last_name": "string",
      "name": "string",
      "company_name": "string",
      "email": "string",
      "phone": "string",
      "mobile": "string",
      "consent": {
        "do_not_email": true,
        "do_not_call": true,
        "do_not_text": true,
        "marketing_opt_in": true
      },
      "url": "string"
    },
    "vehicle_of_interest": {},
    "part_exchange": {},
    "ai": {
      "summary": "string",
      "summary_at": "string",
      "intent_score": 0,
      "intent_stage": "considering",
      "notes": "string",
      "looking_for": {},
      "conversation": {},
      "interested_vehicle_uids": [
        "00000000-0000-0000-0000-000000000000"
      ]
    },
    "enquiry_type": "string",
    "budget": 0,
    "target_price": 0,
    "target_monthly": 0,
    "assignee_uid": "00000000-0000-0000-0000-000000000000",
    "appointment_at": "string",
    "next_action": {
      "at": "string",
      "note": "string"
    },
    "lost_reason": "string",
    "converted_vehicle_uid": "00000000-0000-0000-0000-000000000000",
    "created_at": "string",
    "updated_at": "string",
    "url": "string",
    "stage_history": [
      {}
    ],
    "activities": [
      {}
    ],
    "notes": [
      {}
    ],
    "appointments": [
      {}
    ],
    "tasks": [
      {}
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/leads/LEAD_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
PATCH /v1/dealership/crm/leads/{lead_uid}

Also: /v1/dealerships/{dealership_uid}/crm/leads/{lead_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Update a lead — stage, status, assignee, notes

Moves the lead the way the dealership's own CRM does: setting a `stage` writes the stage change onto the timeline and marks the lead won/lost when the stage is terminal. `note` is appended to the timeline — it never overwrites anything the dealership wrote.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
lead_uid *pathstring
Request body
{
  "stage": "Contacted",
  "appointment_at": "2026-09-14T10:30:00Z",
  "note": "Booked a viewing for Saturday morning."
}
Response 200
{
  "data": {
    "lead_uid": "00000000-0000-0000-0000-000000000000",
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "status": "open",
    "stage": null,
    "source": {
      "type": "ai_chat",
      "raw": "string",
      "conversation_uid": "00000000-0000-0000-0000-000000000000",
      "conversation_url": "string",
      "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
      "engagement_lead_uid": "00000000-0000-0000-0000-000000000000"
    },
    "contact": {
      "contact_uid": "00000000-0000-0000-0000-000000000000",
      "first_name": "string",
      "last_name": "string",
      "name": "string",
      "company_name": "string",
      "email": "string",
      "phone": "string",
      "mobile": "string",
      "consent": {
        "do_not_email": true,
        "do_not_call": true,
        "do_not_text": true,
        "marketing_opt_in": true
      },
      "url": "string"
    },
    "vehicle_of_interest": {},
    "part_exchange": {},
    "ai": {
      "summary": "string",
      "summary_at": "string",
      "intent_score": 0,
      "intent_stage": "considering",
      "notes": "string",
      "looking_for": {},
      "conversation": {},
      "interested_vehicle_uids": [
        "00000000-0000-0000-0000-000000000000"
      ]
    },
    "enquiry_type": "string",
    "budget": 0,
    "target_price": 0,
    "target_monthly": 0,
    "assignee_uid": "00000000-0000-0000-0000-000000000000",
    "appointment_at": "string",
    "next_action": {
      "at": "string",
      "note": "string"
    },
    "lost_reason": "string",
    "converted_vehicle_uid": "00000000-0000-0000-0000-000000000000",
    "created_at": "string",
    "updated_at": "string",
    "url": "string",
    "stage_history": [
      {}
    ],
    "activities": [
      {}
    ],
    "notes": [
      {}
    ],
    "appointments": [
      {}
    ],
    "tasks": [
      {}
    ]
  }
}
cURL
curl -X PATCH "https://api.car-search.ai/v1/dealership/crm/leads/LEAD_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"stage":"Contacted","appointment_at":"2026-09-14T10:30:00Z","note":"Booked a viewing for Saturday morning."}'
POST /v1/dealership/crm/leads/{lead_uid}/notes

Also: /v1/dealerships/{dealership_uid}/crm/leads/{lead_uid}/notes (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Add a note to a lead

Appends a note to the lead's timeline, where the dealership's sales team will see it.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
lead_uid *pathstring
Request body
{
  "body": "Left a voicemail — will try again tomorrow."
}
Response 201
{
  "data": {
    "note_uid": "00000000-0000-0000-0000-000000000000",
    "lead_uid": "00000000-0000-0000-0000-000000000000",
    "body": "string",
    "created_by": "string"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/crm/leads/LEAD_UID/notes" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"body":"Left a voicemail — will try again tomorrow."}'
GET /v1/dealership/crm/contacts

Also: /v1/dealerships/{dealership_uid}/crm/contacts (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List the dealership's contacts

The people behind the leads, with how many leads and AI conversations each one has.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
qquerystringFree text over name, company, email and phone.
updated_sincequerystring
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "contact_uid": "00000000-0000-0000-0000-000000000000",
      "dealership_uid": "00000000-0000-0000-0000-000000000000",
      "first_name": "string",
      "last_name": "string",
      "name": "string",
      "company_name": "string",
      "type": "prospect",
      "email": "string",
      "phone": "string",
      "mobile": "string",
      "address": {},
      "consent": {},
      "source": "string",
      "notes": "string",
      "lead_count": 0,
      "open_lead_count": 0,
      "conversation_count": 0,
      "last_activity_at": "string",
      "created_at": "string",
      "updated_at": "string",
      "url": "string",
      "leads": [
        {
          "lead_uid": "00000000-0000-0000-0000-000000000000",
          "dealership_uid": "00000000-0000-0000-0000-000000000000",
          "status": "open",
          "stage": null,
          "source": {
            "type": "ai_chat",
            "raw": "string",
            "conversation_uid": "00000000-0000-0000-0000-000000000000",
            "conversation_url": "string",
            "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
            "engagement_lead_uid": "00000000-0000-0000-0000-000000000000"
          },
          "contact": {
            "contact_uid": "00000000-0000-0000-0000-000000000000",
            "first_name": "string",
            "last_name": "string",
            "name": "string",
            "company_name": "string",
            "email": "string",
            "phone": "string",
            "mobile": "string",
            "consent": {
              "do_not_email": null,
              "do_not_call": null,
              "do_not_text": null,
              "marketing_opt_in": null
            },
            "url": "string"
          },
          "vehicle_of_interest": {},
          "part_exchange": {},
          "ai": {
            "summary": "string",
            "summary_at": "string",
            "intent_score": 0,
            "intent_stage": "considering",
            "notes": "string",
            "looking_for": {},
            "conversation": {},
            "interested_vehicle_uids": [
              null
            ]
          },
          "enquiry_type": "string",
          "budget": 0,
          "target_price": 0,
          "target_monthly": 0,
          "assignee_uid": "00000000-0000-0000-0000-000000000000",
          "appointment_at": "string",
          "next_action": {
            "at": "string",
            "note": "string"
          },
          "lost_reason": "string",
          "converted_vehicle_uid": "00000000-0000-0000-0000-000000000000",
          "created_at": "string",
          "updated_at": "string",
          "url": "string",
          "stage_history": [
            {}
          ],
          "activities": [
            {}
          ],
          "notes": [
            {}
          ],
          "appointments": [
            {}
          ],
          "tasks": [
            {}
          ]
        }
      ],
      "conversations": [
        {}
      ],
      "vehicle_interests": [
        {}
      ],
      "activities": [
        {}
      ]
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 0,
    "total_count": 0,
    "total_results": 0
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/contacts" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/crm/contacts/{contact_uid}

Also: /v1/dealerships/{dealership_uid}/crm/contacts/{contact_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

One contact, with their leads and conversations

The contact plus every lead of theirs, every AI conversation they had (each with a link to the transcript), the vehicles they showed interest in, and their timeline.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
contact_uid *pathstring
Response 200
{
  "data": {
    "contact_uid": "00000000-0000-0000-0000-000000000000",
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "first_name": "string",
    "last_name": "string",
    "name": "string",
    "company_name": "string",
    "type": "prospect",
    "email": "string",
    "phone": "string",
    "mobile": "string",
    "address": {},
    "consent": {},
    "source": "string",
    "notes": "string",
    "lead_count": 0,
    "open_lead_count": 0,
    "conversation_count": 0,
    "last_activity_at": "string",
    "created_at": "string",
    "updated_at": "string",
    "url": "string",
    "leads": [
      {
        "lead_uid": "00000000-0000-0000-0000-000000000000",
        "dealership_uid": "00000000-0000-0000-0000-000000000000",
        "status": "open",
        "stage": null,
        "source": {
          "type": "ai_chat",
          "raw": "string",
          "conversation_uid": "00000000-0000-0000-0000-000000000000",
          "conversation_url": "string",
          "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
          "engagement_lead_uid": "00000000-0000-0000-0000-000000000000"
        },
        "contact": {
          "contact_uid": "00000000-0000-0000-0000-000000000000",
          "first_name": "string",
          "last_name": "string",
          "name": "string",
          "company_name": "string",
          "email": "string",
          "phone": "string",
          "mobile": "string",
          "consent": {
            "do_not_email": true,
            "do_not_call": true,
            "do_not_text": true,
            "marketing_opt_in": true
          },
          "url": "string"
        },
        "vehicle_of_interest": {},
        "part_exchange": {},
        "ai": {
          "summary": "string",
          "summary_at": "string",
          "intent_score": 0,
          "intent_stage": "considering",
          "notes": "string",
          "looking_for": {},
          "conversation": {},
          "interested_vehicle_uids": [
            "00000000-0000-0000-0000-000000000000"
          ]
        },
        "enquiry_type": "string",
        "budget": 0,
        "target_price": 0,
        "target_monthly": 0,
        "assignee_uid": "00000000-0000-0000-0000-000000000000",
        "appointment_at": "string",
        "next_action": {
          "at": "string",
          "note": "string"
        },
        "lost_reason": "string",
        "converted_vehicle_uid": "00000000-0000-0000-0000-000000000000",
        "created_at": "string",
        "updated_at": "string",
        "url": "string",
        "stage_history": [
          {}
        ],
        "activities": [
          {}
        ],
        "notes": [
          {}
        ],
        "appointments": [
          {}
        ],
        "tasks": [
          {}
        ]
      }
    ],
    "conversations": [
      {}
    ],
    "vehicle_interests": [
      {}
    ],
    "activities": [
      {}
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/contacts/CONTACT_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/crm/part-exchanges

Also: /v1/dealerships/{dealership_uid}/crm/part-exchanges (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List part-exchanges

Every trade-in the dealership has been offered, whether it came from the `oc-part-exchange` component, the website, or your own API call.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
statusquerystring (open | converted | won | lost)`open` = not yet taken into stock; `converted` = accepted into inventory.
updated_sincequerystring
qquerystringFree text over the customer and the car (reg, VIN, make, model).
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "part_exchange_uid": "23ef792d-5b07-4173-8abe-8926f8c1eeb0",
      "lead_uid": "23ef792d-5b07-4173-8abe-8926f8c1eeb0",
      "dealership_uid": "DEALERSHIP_UID",
      "status": "open",
      "accepted": false,
      "appraisal_status": "appraisal",
      "stage": {
        "stage_uid": "b1c2d3e4-0000-4000-8000-000000000001",
        "name": "New",
        "sort_order": 1,
        "is_terminal": false,
        "is_won": false
      },
      "contact": {
        "contact_uid": "aa11bb22-cc33-4d44-8e55-ff6677889900",
        "name": "Sam Trade",
        "email": "sam@example.com",
        "phone": "07700 900789",
        "url": "/v1/dealerships/DEALERSHIP_UID/crm/contacts/aa11bb22-cc33-4d44-8e55-ff6677889900"
      },
      "vehicle": {
        "vrm": "AB12CDE",
        "vin": "WF0AXXGCDA12345678",
        "make": "Ford",
        "model": "Focus",
        "year": 2015,
        "mileage": 48000
      },
      "appraisal": {
        "vehicle_uid": "99887766-5544-4332-2110-aabbccddeeff",
        "make": "Ford",
        "model": "Focus",
        "variant": "1.0 EcoBoost Zetec",
        "year": 2015,
        "mileage": 48000,
        "condition_grade": "B",
        "condition_notes": "Kerbed nearside alloy; small scuff on the rear bumper.",
        "stock_status": "appraisal"
      },
      "valuation": {
        "amount": 4750,
        "currency": "GBP"
      },
      "photo_count": 6,
      "converted_vehicle_uid": "99887766-5544-4332-2110-aabbccddeeff",
      "source": {
        "conversation_uid": null,
        "conversation_url": null
      },
      "created_at": "2026-09-11T16:20:00Z",
      "updated_at": "2026-09-11T16:41:12Z",
      "url": "/v1/dealerships/DEALERSHIP_UID/crm/part-exchanges/23ef792d-5b07-4173-8abe-8926f8c1eeb0",
      "lead_url": "/v1/dealerships/DEALERSHIP_UID/crm/leads/23ef792d-5b07-4173-8abe-8926f8c1eeb0"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/part-exchanges" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/crm/part-exchanges/{part_exchange_uid}

Also: /v1/dealerships/{dealership_uid}/crm/part-exchanges/{part_exchange_uid} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

One part-exchange — photos, AI condition review, valuation

The customer's car as identified, every photo with what the AI read off it (shot type, description, plate / VIN / odometer), the condition notes and the valuation. The same record is also a lead: see `lead_url`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
part_exchange_uid *pathstring
Response 200
{
  "data": {
    "part_exchange_uid": "23ef792d-5b07-4173-8abe-8926f8c1eeb0",
    "lead_uid": "23ef792d-5b07-4173-8abe-8926f8c1eeb0",
    "dealership_uid": "DEALERSHIP_UID",
    "status": "open",
    "accepted": false,
    "appraisal_status": "appraisal",
    "stage": {
      "stage_uid": "b1c2d3e4-0000-4000-8000-000000000001",
      "name": "New",
      "sort_order": 1,
      "is_terminal": false,
      "is_won": false
    },
    "contact": {
      "contact_uid": "aa11bb22-cc33-4d44-8e55-ff6677889900",
      "name": "Sam Trade",
      "email": "sam@example.com",
      "phone": "07700 900789",
      "url": "/v1/dealerships/DEALERSHIP_UID/crm/contacts/aa11bb22-cc33-4d44-8e55-ff6677889900"
    },
    "vehicle": {
      "vrm": "AB12CDE",
      "vin": "WF0AXXGCDA12345678",
      "make": "Ford",
      "model": "Focus",
      "year": 2015,
      "mileage": 48000
    },
    "appraisal": {
      "vehicle_uid": "99887766-5544-4332-2110-aabbccddeeff",
      "make": "Ford",
      "model": "Focus",
      "variant": "1.0 EcoBoost Zetec",
      "year": 2015,
      "mileage": 48000,
      "condition_grade": "B",
      "condition_notes": "Kerbed nearside alloy; small scuff on the rear bumper.",
      "stock_status": "appraisal"
    },
    "valuation": {
      "amount": 4750,
      "currency": "GBP"
    },
    "photo_count": 6,
    "converted_vehicle_uid": "99887766-5544-4332-2110-aabbccddeeff",
    "source": {
      "conversation_uid": null,
      "conversation_url": null
    },
    "created_at": "2026-09-11T16:20:00Z",
    "updated_at": "2026-09-11T16:41:12Z",
    "url": "/v1/dealerships/DEALERSHIP_UID/crm/part-exchanges/23ef792d-5b07-4173-8abe-8926f8c1eeb0",
    "lead_url": "/v1/dealerships/DEALERSHIP_UID/crm/leads/23ef792d-5b07-4173-8abe-8926f8c1eeb0"
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/part-exchanges/PART_EXCHANGE_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/crm/appointments

Also: /v1/dealerships/{dealership_uid}/crm/appointments (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List appointments

The bookings on the dealership's leads — viewings, test drives and callbacks. Defaults to everything from now onwards; pass `from`/`to` for any window.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
fromquerystringDefaults to now. Send an empty value for no lower bound.
toquerystring
statusquerystring (open | won | lost)
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "appointment_at": "2026-09-14T10:30:00Z",
      "lead_uid": "7f2b1d84-3c55-4f6e-9a11-2b8e0c9d4a01",
      "lead_url": "/v1/dealerships/DEALERSHIP_UID/crm/leads/7f2b1d84-3c55-4f6e-9a11-2b8e0c9d4a01",
      "status": "open",
      "stage": {
        "stage_uid": "b1c2d3e4-0000-4000-8000-000000000002",
        "name": "Contacted",
        "sort_order": 2,
        "is_terminal": false,
        "is_won": false
      },
      "note": "Call to confirm the viewing",
      "assignee_uid": null,
      "contact": {
        "contact_uid": "3a9f0e12-77bb-4c31-9d55-0f1e2d3c4b5a",
        "name": "Casey Brown",
        "email": "casey@example.com",
        "phone": "07700 900123"
      },
      "vehicle": {
        "vehicle_uid": "11111111-2222-3333-4444-555555555555",
        "make": "Kia",
        "model": "Sportage",
        "vrm": "AB21CDE"
      },
      "updated_at": "2026-09-11T15:10:02Z"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/appointments" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/crm/pipeline

Also: /v1/dealerships/{dealership_uid}/crm/pipeline (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

The dealership's pipeline stages with lead counts

Each dealership names its own stages. Use the `stage_uid`s here when filtering leads or moving one with `PATCH /crm/leads/{lead_uid}`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Response 200
{
  "data": {
    "stages": [
      {
        "stage_uid": "b1c2d3e4-0000-4000-8000-000000000001",
        "name": "New Lead",
        "sort_order": 1,
        "is_terminal": false,
        "is_won": false,
        "lead_count": 12,
        "open_lead_count": 9
      },
      {
        "stage_uid": "b1c2d3e4-0000-4000-8000-000000000002",
        "name": "Contacted",
        "sort_order": 2,
        "is_terminal": false,
        "is_won": false,
        "lead_count": 6,
        "open_lead_count": 6
      }
    ],
    "total_leads": 18,
    "total_open_leads": 15
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/crm/pipeline" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

Part exchange

POST /v1/dealership/part-exchange

Also: /v1/dealerships/{dealership_uid}/part-exchange (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Start a part-exchange / trade-in appraisal

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "contact": {
    "first_name": "string",
    "last_name": "string",
    "email": "you@example.com",
    "phone": "string"
  },
  "vehicle": {
    "reg": "string",
    "vin": "string",
    "mileage": 0
  },
  "notes": "string"
}
Response 201
{
  "data": {
    "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
    "vehicle_uid": "00000000-0000-0000-0000-000000000000",
    "customer_uid": "00000000-0000-0000-0000-000000000000",
    "upload_token": "string",
    "checklist": [
      {}
    ]
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"contact":{"first_name":"string","last_name":"string","email":"you@example.com","phone":"string"},"vehicle":{"reg":"string","vin":"string","mileage":0},"notes":"string"}'
POST /v1/dealership/part-exchange/{id}/photos

Also: /v1/dealerships/{dealership_uid}/part-exchange/{id}/photos (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Upload guided photos

Multipart photo upload (field `photos`, up to 10 per request; optional `shot_id` to pin a slot: see the checklist returned by the start call). Each photo is read by AI as it arrives (registration/VIN plate, odometer) and the full appraisal is scheduled a short debounce after the last one — or call `/process` to force it now.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
id *pathstringThe part_exchange_uid returned by the start call.
Response 201
{
  "data": {
    "photos": [
      {
        "filename": "string",
        "image_uid": "string",
        "kept_in": "string",
        "ai_shot_type": "string",
        "confidence": 0,
        "description": "string",
        "reading": {
          "vin": "string",
          "vrm": "string",
          "odometer": 0
        }
      }
    ],
    "checklist": [
      {}
    ],
    "complete": true
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID/photos" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "files[]=@/path/to/photo.jpg"
POST /v1/dealership/part-exchange/{id}/process

Also: /v1/dealerships/{dealership_uid}/part-exchange/{id}/process (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Run the full appraisal now

By default the appraisal runs automatically a short debounce after the last photo; call this to force it immediately. Runs as a background job — poll `GET /v1/jobs/{job_uid}`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
id *pathstringThe part_exchange_uid returned by the start call.
Response 202
{
  "data": {
    "job_uid": "00000000-0000-0000-0000-000000000000",
    "status": "queued",
    "poll": "string",
    "file": "string"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID/process" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
GET /v1/dealership/part-exchange/{id}

Also: /v1/dealerships/{dealership_uid}/part-exchange/{id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get the appraisal

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
id *pathstringThe part_exchange_uid returned by the start call.
Response 200
{
  "data": {
    "part_exchange_uid": "00000000-0000-0000-0000-000000000000",
    "status": "string",
    "created_at": "string",
    "lead": {
      "customer_uid": "string",
      "first_name": "string",
      "last_name": "string",
      "email": "string",
      "phone": "string"
    },
    "vehicle": {},
    "valuation": {
      "trade_value": 0,
      "currency": "string"
    },
    "photos": [
      null
    ],
    "checklist": [
      {}
    ],
    "complete": true,
    "photo_count": 0
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/part-exchange/PART_EXCHANGE_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"

Showroom images

GET /v1/dealership/showroom-backdrops

Also: /v1/dealerships/{dealership_uid}/showroom-backdrops (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

List showroom backdrops

Returns this dealership's own uploaded backdrops plus the platform's default studio backdrops.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Response 200
{
  "data": [
    {
      "backdrop_uid": "string",
      "kind": "dealership",
      "name": "string",
      "width": 0,
      "height": 0,
      "byte_size": 0,
      "thumb_url": "string",
      "url": "string"
    }
  ]
}
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/showroom-backdrops" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/showroom-backdrops

Also: /v1/dealerships/{dealership_uid}/showroom-backdrops (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Upload a showroom backdrop

Multipart files (field `images`, up to 5), or JSON `{url, name?}`.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Response 201
{
  "data": [
    {
      "backdrop_uid": "string",
      "name": "string",
      "width": 0,
      "height": 0
    }
  ]
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/showroom-backdrops" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -F "files[]=@/path/to/photo.jpg"
GET /v1/dealership/showroom-backdrops/{id}

Also: /v1/dealerships/{dealership_uid}/showroom-backdrops/{id} (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Get a platform default backdrop image

Serves the bytes of a platform default backdrop (id like `platform:studio-light`, from the list call). A dealership's own backdrops are served via `GET /v1/files/{uid}?kind=backdrop&dealership_uid=...` instead.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
id *pathstring
sizequerystring (full | thumb)
cURL
curl -X GET "https://api.car-search.ai/v1/dealership/showroom-backdrops/PART_EXCHANGE_ID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID"
POST /v1/dealership/showroom-images

Also: /v1/dealerships/{dealership_uid}/showroom-images (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Generate a showroom image

Cuts the car out of the source photo, composites it onto the chosen backdrop and optionally runs an AI lighting-harmonize pass. Runs as a background job (`202`) — poll `GET /v1/jobs/{job_uid}`, then fetch the image from `GET /v1/files/{job_uid}` once done.

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "image_uid": "00000000-0000-0000-0000-000000000000",
  "url": "https://example.com",
  "backdrop_uid": "string",
  "vehicle_uid": "00000000-0000-0000-0000-000000000000",
  "options": {
    "width_fraction": 0,
    "position": {
      "x": 0,
      "y": 0
    },
    "light": {},
    "harmonize": true
  }
}
Response 202
{
  "data": {
    "job_uid": "00000000-0000-0000-0000-000000000000",
    "status": "queued",
    "poll": "string",
    "file": "string"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/showroom-images" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"image_uid":"00000000-0000-0000-0000-000000000000","url":"https://example.com","backdrop_uid":"string","vehicle_uid":"00000000-0000-0000-0000-000000000000","options":{"width_fraction":0,"position":{"x":0,"y":0},"light":{},"harmonize":true}}'
POST /v1/dealership/showroom-images/{job_uid}/accept

Also: /v1/dealerships/{dealership_uid}/showroom-images/{job_uid}/accept (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Attach the result to a vehicle

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
job_uid *pathstring
Request body
{
  "vehicle_uid": "00000000-0000-0000-0000-000000000000"
}
Response 201
{
  "data": {
    "image_uid": "string",
    "vehicle_uid": "string",
    "url": "string"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/showroom-images/JOB_UID/accept" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"vehicle_uid":"00000000-0000-0000-0000-000000000000"}'

Jobs & files

Background jobs (showroom images, part-exchange processing) and the files they produce.

GET /v1/jobs/{job_uid}

Requires the partner key only.

Poll a background job

Used by the part-exchange (`px_appraisal`) and showroom-image (`showroom`) flows. A large result (e.g. the showroom image bytes) is stripped from `output` unless `?inline=1` — otherwise fetch it from `GET /v1/files/{job_uid}`.

NameInTypeDescription
job_uid *pathstring
inlinequeryinteger (0 | 1)
Response 200
{
  "data": {
    "job_uid": "00000000-0000-0000-0000-000000000000",
    "dealership_uid": "00000000-0000-0000-0000-000000000000",
    "kind": "showroom",
    "status": "queued",
    "input": {},
    "output": {},
    "error": "string",
    "created_at": "string",
    "started_at": "string",
    "finished_at": "string"
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/jobs/JOB_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /v1/files/{uid}

Requires the partner key only.

Fetch a file by uid

One URL shape for every binary the API hands out: a vehicle photo (`?size=full|medium|thumb`), a showroom job result, or a dealership backdrop (`?kind=backdrop&dealership_uid=...`).

NameInTypeDescription
uid *pathstring
kindquerystring (backdrop)
dealership_uidquerystring
sizequerystring (full | medium | thumb)
cURL
curl -X GET "https://api.car-search.ai/v1/files/UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"

Leads

POST /v1/dealership/leads

Also: /v1/dealerships/{dealership_uid}/leads (the dealership UID in the path instead of the header). Requires the partner key and the dealership UID (X-Dealership-Uid).

Capture a lead

NameInTypeDescription
dealership_uid *pathstringThe dealership's UID (Partners portal → API → Your dealerships). The header form of this endpoint (`x-header-form-path`) drops this segment and sends `X-Dealership-Uid` instead.
Request body
{
  "name": "string",
  "email": "you@example.com",
  "phone": "string",
  "message": "string",
  "vehicle_uid": "00000000-0000-0000-0000-000000000000"
}
Response 201
{
  "data": {
    "lead_uid": "00000000-0000-0000-0000-000000000000",
    "customer_uid": "00000000-0000-0000-0000-000000000000"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/dealership/leads" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "X-Dealership-Uid: DEALERSHIP_UID" \
  -H "Content-Type: application/json" \
  -d '{"name":"string","email":"you@example.com","phone":"string","message":"string","vehicle_uid":"00000000-0000-0000-0000-000000000000"}'

Webhooks

Get told when something happens at one of your dealerships instead of polling for it. One https endpoint can receive events for every dealership you support. Every delivery is signed with that endpoint's own secret.

GET /v1/webhooks

Requires the partner key only.

List your webhook subscriptions

Every endpoint you have registered, plus the full list of events you can subscribe to.

Response 200
{
  "data": [
    {
      "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
      "url": "https://your-app.example/hooks/carsearch",
      "description": "Main CRM sync",
      "events": [
        "lead.created",
        "lead.updated",
        "part_exchange.created"
      ],
      "all_events": false,
      "dealership_uids": [
        "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42"
      ],
      "dealerships": [
        {
          "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
          "name": "Example Motors"
        }
      ],
      "all_dealerships": false,
      "status": "active",
      "failure_count": 0,
      "disabled_reason": null,
      "disabled_at": null,
      "secret_rotated_at": null,
      "last_delivery_at": "2026-09-12T09:51:45.201Z",
      "delivered_count": 42,
      "failed_count": 1,
      "pending_count": 0,
      "created_at": "2026-08-01T10:00:00Z",
      "updated_at": "2026-09-12T09:51:45.201Z",
      "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42"
    }
  ],
  "available_events": [
    "conversation.started",
    "conversation.ended",
    "conversation.summarised",
    "lead.created",
    "lead.updated",
    "part_exchange.created",
    "part_exchange.photos_reviewed",
    "part_exchange.appraised",
    "listing.enhanced.ready",
    "showroom.ready",
    "*"
  ]
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks

Requires the partner key only.

Create a webhook subscription

Registers an https endpoint and we start sending it events. One endpoint can cover every dealership you support, or you can pin it to a chosen few with `dealership_uids`. Each delivery is a POST of `{ event, delivery_uid, occurred_at, dealership_uid, data, links }`. `data` carries uids and a few low cardinality fields such as status, source or stage name. `links` is a set of ready made API paths to read the full record. A delivery never carries a customer's name, email, phone number or chat transcript; read those back with your partner key. Every delivery is signed: `X-CSAI-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`. Verify it by recomputing v1 over the raw request body, not the parsed one. A failed delivery is retried after 2, 4, 8 and then 16 minutes, five attempts in all. An endpoint that fails ten times in a row is disabled; fix it and resume it. `secret` is returned once, at creation. Store it; we do not show it again. Use rotate-secret if you lose it.

Request body
{
  "url": "https://your-app.example/hooks/carsearch",
  "description": "Main CRM sync",
  "events": [
    "lead.created",
    "lead.updated",
    "part_exchange.created"
  ]
}
Response 201
{
  "data": {
    "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
    "url": "https://your-app.example/hooks/carsearch",
    "description": "Main CRM sync",
    "events": [
      "lead.created",
      "lead.updated",
      "part_exchange.created"
    ],
    "all_events": false,
    "dealership_uids": [
      "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42"
    ],
    "dealerships": [
      {
        "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
        "name": "Example Motors"
      }
    ],
    "all_dealerships": false,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": null,
    "disabled_at": null,
    "secret_rotated_at": null,
    "last_delivery_at": null,
    "delivered_count": 0,
    "failed_count": 0,
    "pending_count": 0,
    "created_at": "2026-08-01T10:00:00Z",
    "updated_at": "2026-09-12T09:51:45.201Z",
    "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
    "secret": "whsec_9f3a7c2e1b6d4f80a5c3e9b1d7f24680",
    "signature_header": "X-CSAI-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\">"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example/hooks/carsearch","description":"Main CRM sync","events":["lead.created","lead.updated","part_exchange.created"]}'
GET /v1/webhooks/{webhook_uid}

Requires the partner key only.

One webhook subscription

The endpoint as it stands: its events, the dealerships it covers and its delivery counts.

NameInTypeDescription
webhook_uid *pathstring
Response 200
{
  "data": {
    "webhook_uid": "00000000-0000-0000-0000-000000000000",
    "url": "string",
    "description": "string",
    "events": [
      "conversation.started"
    ],
    "all_events": true,
    "dealership_uids": [
      "00000000-0000-0000-0000-000000000000"
    ],
    "dealerships": [
      {
        "dealership_uid": "00000000-0000-0000-0000-000000000000",
        "name": "string"
      }
    ],
    "all_dealerships": true,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": "string",
    "disabled_at": "string",
    "secret_rotated_at": "string",
    "last_delivery_at": "string",
    "delivered_count": 0,
    "failed_count": 0,
    "pending_count": 0,
    "created_at": "string",
    "updated_at": "string",
    "dealership_uid": "00000000-0000-0000-0000-000000000000"
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
PATCH /v1/webhooks/{webhook_uid}

Requires the partner key only.

Update a webhook subscription

Changes the url, description, events or the dealerships it covers. Only the fields you send are changed.

NameInTypeDescription
webhook_uid *pathstring
Request body
{
  "events": [
    "lead.created",
    "lead.updated",
    "part_exchange.created",
    "part_exchange.appraised"
  ]
}
Response 200
{
  "data": {
    "webhook_uid": "00000000-0000-0000-0000-000000000000",
    "url": "string",
    "description": "string",
    "events": [
      "conversation.started"
    ],
    "all_events": true,
    "dealership_uids": [
      "00000000-0000-0000-0000-000000000000"
    ],
    "dealerships": [
      {
        "dealership_uid": "00000000-0000-0000-0000-000000000000",
        "name": "string"
      }
    ],
    "all_dealerships": true,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": "string",
    "disabled_at": "string",
    "secret_rotated_at": "string",
    "last_delivery_at": "string",
    "delivered_count": 0,
    "failed_count": 0,
    "pending_count": 0,
    "created_at": "string",
    "updated_at": "string",
    "dealership_uid": "00000000-0000-0000-0000-000000000000"
  }
}
cURL
curl -X PATCH "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["lead.created","lead.updated","part_exchange.created","part_exchange.appraised"]}'
DELETE /v1/webhooks/{webhook_uid}

Requires the partner key only.

Delete a webhook subscription

Removes the endpoint: it leaves your list at once, nothing more is sent to it, and it can no longer be updated, paused, tested or given a new secret. Its delivery log is kept for audit — read it with `GET /v1/webhooks/deliveries?webhook_uid=…` or `GET /v1/webhooks/deliveries/{delivery_uid}`; a retry of one of its deliveries is refused with 409.

NameInTypeDescription
webhook_uid *pathstring
Response 200
{
  "data": {
    "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
    "deleted": true,
    "status": "deleted",
    "deliveries": "/v1/webhooks/deliveries?webhook_uid=1bcd2b26-28ac-45e8-b291-5d87246813e8"
  }
}
cURL
curl -X DELETE "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /v1/webhooks/{webhook_uid}/deliveries

Requires the partner key only.

The delivery log for one webhook

What we sent, what your endpoint answered and when the next retry is due, for one endpoint. The first place to look when an integration goes quiet.

NameInTypeDescription
webhook_uid *pathstring
statusquerystring (pending | sending | delivered | failed)
eventquerystring (conversation.started | conversation.ended | conversation.summarised | lead.created | lead.updated | part_exchange.created | part_exchange.photos_reviewed | part_exchange.appraised | listing.enhanced.ready | showroom.ready)
dealership_uidquerystringOnly deliveries for this dealership.
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "delivery_uid": "b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07",
      "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
      "webhook_url": "https://your-app.example/hooks/carsearch",
      "webhook_description": "Main CRM sync",
      "event": "lead.created",
      "status": "delivered",
      "attempts": 1,
      "status_code": 200,
      "duration_ms": 184,
      "error": null,
      "is_test": false,
      "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
      "dealership_name": "Example Motors",
      "next_attempt_at": null,
      "delivered_at": "2026-09-12T09:51:45.201Z",
      "created_at": "2026-09-12T09:51:44.301Z",
      "updated_at": "2026-09-12T09:51:45.201Z",
      "response_preview": "OK"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/deliveries" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /v1/webhooks/events

Requires the partner key only.

List the events you can subscribe to

The full catalogue: what each event means, which uids it carries in `data` and a sample body, plus how signing and retries work. Read this instead of hard coding a list; subscribing to `*` includes events we add after you subscribed.

Response 200
{
  "data": [
    {
      "event": "lead.created",
      "group": "Leads",
      "title": "Lead created",
      "description": "A new lead reached the dealership CRM, from the AI chat, a website form or your own API call.",
      "uids": [
        "lead_uid",
        "contact_uid",
        "conversation_uid"
      ],
      "sample": {
        "event": "lead.created",
        "delivery_uid": "b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07",
        "occurred_at": "2026-09-12T09:51:44.006Z",
        "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
        "data": {
          "lead_uid": "9c4a77b2-1d3e-4a58-b0c6-8e2f5a9d7c31",
          "contact_uid": "7b2d4e19-5c8a-4b31-9f6d-2a7c1e0b5d84",
          "conversation_uid": "a1f3c8d2-6e94-4b70-8d15-3c9b7e2f4a60",
          "source": "ai_chat",
          "enquiry_type": "sales",
          "status": "open",
          "stage_name": "New enquiry",
          "vehicle_uid": "6a0c9f45-2b18-4d73-95ea-7c31f8b04d26",
          "created_at": "2026-09-12T09:51:44.006Z",
          "sample": true
        },
        "links": {
          "lead": "/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/crm/leads/9c4a77b2-1d3e-4a58-b0c6-8e2f5a9d7c31",
          "contact": "/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/crm/contacts/7b2d4e19-5c8a-4b31-9f6d-2a7c1e0b5d84",
          "conversation": "/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/conversations/a1f3c8d2-6e94-4b70-8d15-3c9b7e2f4a60",
          "vehicle": "/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/inventory/6a0c9f45-2b18-4d73-95ea-7c31f8b04d26"
        }
      }
    }
  ],
  "signature": {
    "header": "X-CSAI-Signature",
    "scheme": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\">",
    "tolerance_seconds": 300
  },
  "retries": {
    "attempts": 5,
    "after_minutes": [
      2,
      4,
      8,
      16
    ],
    "disabled_after_consecutive_failures": 10
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks/events" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /v1/webhooks/deliveries

Requires the partner key only.

List deliveries across every webhook

The delivery log for your whole account, across every endpoint. Filter by webhook, status, event or dealership; this is the first place to look when an integration goes quiet.

NameInTypeDescription
webhook_uidquerystringOnly this webhook's deliveries.
statusquerystring (pending | sending | delivered | failed)
eventquerystring (conversation.started | conversation.ended | conversation.summarised | lead.created | lead.updated | part_exchange.created | part_exchange.photos_reviewed | part_exchange.appraised | listing.enhanced.ready | showroom.ready)
dealership_uidquerystringOnly deliveries for this dealership.
offsetqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "delivery_uid": "b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07",
      "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
      "webhook_url": "https://your-app.example/hooks/carsearch",
      "webhook_description": "Main CRM sync",
      "event": "lead.created",
      "status": "delivered",
      "attempts": 1,
      "status_code": 200,
      "duration_ms": 184,
      "error": null,
      "is_test": false,
      "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
      "dealership_name": "Example Motors",
      "next_attempt_at": null,
      "delivered_at": "2026-09-12T09:51:45.201Z",
      "created_at": "2026-09-12T09:51:44.301Z",
      "updated_at": "2026-09-12T09:51:45.201Z",
      "response_preview": "OK"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 50,
    "total_count": 1
  }
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks/deliveries" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /v1/webhooks/deliveries/{delivery_uid}

Requires the partner key only.

One delivery, request and response included

The exact request we sent and what your endpoint answered, for one delivery. The signing secret is never in it; only the signature we computed with it.

NameInTypeDescription
delivery_uid *pathstring
Response 200
{
  "data": null
}
cURL
curl -X GET "https://api.car-search.ai/v1/webhooks/deliveries/DELIVERY_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks/deliveries/{delivery_uid}/retry

Requires the partner key only.

Send a delivery again now

Starts the attempt schedule over for one delivery, right away, instead of waiting for its next scheduled retry.

NameInTypeDescription
delivery_uid *pathstring
Response 200
{
  "data": {
    "delivery_uid": "b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07",
    "status": "pending"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks/deliveries/DELIVERY_UID/retry" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks/{webhook_uid}/pause

Requires the partner key only.

Pause a webhook

Stops sending without losing the endpoint, its events or its secret. Resume it whenever you are ready.

NameInTypeDescription
webhook_uid *pathstring
Response 200
{
  "data": {
    "webhook_uid": "00000000-0000-0000-0000-000000000000",
    "url": "string",
    "description": "string",
    "events": [
      "conversation.started"
    ],
    "all_events": true,
    "dealership_uids": [
      "00000000-0000-0000-0000-000000000000"
    ],
    "dealerships": [
      {
        "dealership_uid": "00000000-0000-0000-0000-000000000000",
        "name": "string"
      }
    ],
    "all_dealerships": true,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": "string",
    "disabled_at": "string",
    "secret_rotated_at": "string",
    "last_delivery_at": "string",
    "delivered_count": 0,
    "failed_count": 0,
    "pending_count": 0,
    "created_at": "string",
    "updated_at": "string",
    "dealership_uid": "00000000-0000-0000-0000-000000000000"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/pause" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks/{webhook_uid}/resume

Requires the partner key only.

Resume a webhook

Starts sending again. If we disabled this endpoint ourselves after repeated failures, this also clears that failure run, so a fixed endpoint gets a clean start.

NameInTypeDescription
webhook_uid *pathstring
Response 200
{
  "data": {
    "webhook_uid": "00000000-0000-0000-0000-000000000000",
    "url": "string",
    "description": "string",
    "events": [
      "conversation.started"
    ],
    "all_events": true,
    "dealership_uids": [
      "00000000-0000-0000-0000-000000000000"
    ],
    "dealerships": [
      {
        "dealership_uid": "00000000-0000-0000-0000-000000000000",
        "name": "string"
      }
    ],
    "all_dealerships": true,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": "string",
    "disabled_at": "string",
    "secret_rotated_at": "string",
    "last_delivery_at": "string",
    "delivered_count": 0,
    "failed_count": 0,
    "pending_count": 0,
    "created_at": "string",
    "updated_at": "string",
    "dealership_uid": "00000000-0000-0000-0000-000000000000"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/resume" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks/{webhook_uid}/rotate-secret

Requires the partner key only.

Rotate the signing secret

Issues a new secret and returns it once. The old secret stops verifying deliveries immediately, so switch your receiver over as soon as you have the new one.

NameInTypeDescription
webhook_uid *pathstring
Response 200
{
  "data": {
    "webhook_uid": "1bcd2b26-28ac-45e8-b291-5d87246813e8",
    "url": "https://your-app.example/hooks/carsearch",
    "description": "Main CRM sync",
    "events": [
      "lead.created",
      "lead.updated",
      "part_exchange.created"
    ],
    "all_events": false,
    "dealership_uids": [
      "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42"
    ],
    "dealerships": [
      {
        "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
        "name": "Example Motors"
      }
    ],
    "all_dealerships": false,
    "status": "active",
    "failure_count": 0,
    "disabled_reason": null,
    "disabled_at": null,
    "secret_rotated_at": "2026-09-12T10:15:00Z",
    "last_delivery_at": "2026-09-12T09:51:45.201Z",
    "delivered_count": 42,
    "failed_count": 1,
    "pending_count": 0,
    "created_at": "2026-08-01T10:00:00Z",
    "updated_at": "2026-09-12T09:51:45.201Z",
    "dealership_uid": "2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42",
    "secret": "whsec_2a6c1f9e0b4d7385a1c9e5b3d7f04862"
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/rotate-secret" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /v1/webhooks/{webhook_uid}/test

Requires the partner key only.

Send a test delivery

Sends a real, signed delivery of a sample body for one event, right away, and reports what your endpoint answered. It appears in the delivery log marked as a test. It never retries and never counts toward the failures that disable an endpoint.

NameInTypeDescription
webhook_uid *pathstring
Request body
{
  "event": "lead.created"
}
Response 200
{
  "data": {
    "delivery_uid": "b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07",
    "event": "lead.created",
    "url": "https://your-app.example/hooks/carsearch",
    "delivered": true,
    "status_code": 200,
    "duration_ms": 184,
    "response_body": "OK",
    "response_truncated": false,
    "error": null,
    "signature": "t=1757668304,v1=9c2a7f1e0b4d5386a1c9e5b3d7f04862fa63e0b1d2c4e6f8091a2b3c4d5e6f70",
    "sent_body": "{\"event\":\"lead.created\",\"delivery_uid\":\"b8c1e0d4-7a35-4f29-9c60-2e5d8b3a1f07\",\"occurred_at\":\"2026-09-12T09:51:44.006Z\",\"dealership_uid\":\"2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42\",\"data\":{\"lead_uid\":\"9c4a77b2-1d3e-4a58-b0c6-8e2f5a9d7c31\",\"contact_uid\":\"7b2d4e19-5c8a-4b31-9f6d-2a7c1e0b5d84\",\"conversation_uid\":\"a1f3c8d2-6e94-4b70-8d15-3c9b7e2f4a60\",\"source\":\"ai_chat\",\"enquiry_type\":\"sales\",\"status\":\"open\",\"stage_name\":\"New enquiry\",\"vehicle_uid\":\"6a0c9f45-2b18-4d73-95ea-7c31f8b04d26\",\"created_at\":\"2026-09-12T09:51:44.006Z\",\"sample\":true},\"links\":{\"lead\":\"/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/crm/leads/9c4a77b2-1d3e-4a58-b0c6-8e2f5a9d7c31\",\"contact\":\"/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/crm/contacts/7b2d4e19-5c8a-4b31-9f6d-2a7c1e0b5d84\",\"conversation\":\"/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/conversations/a1f3c8d2-6e94-4b70-8d15-3c9b7e2f4a60\",\"vehicle\":\"/v1/dealerships/2f1e5c60-9a1b-4f7d-8c2e-6b0a5d3e1f42/inventory/6a0c9f45-2b18-4d73-95ea-7c31f8b04d26\"}}",
    "message": "Your endpoint answered 200 in 184 ms."
  }
}
cURL
curl -X POST "https://api.car-search.ai/v1/webhooks/WEBHOOK_UID/test" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"lead.created"}'

Embedding

GET /widgets/loader.js

Requires the partner key only.

Component loader script

Referenced by `<script src>` on the dealer site — never called directly by your backend. See the Embedding guide.

cURL
curl -X GET "https://api.car-search.ai/widgets/loader.js" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
GET /embed/{widget_uid}

Requires the partner key only.

Embedded component frame

NameInTypeDescription
widget_uid *pathstring
cURL
curl -X GET "https://api.car-search.ai/embed/WIDGET_UID" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"
POST /api/embed/exchange

Requires the partner key only.

Exchange a publishable key for a short-lived embed session

Called by the loader script itself, from the dealer's page. The publishable key (`oc_publishable_…`) is public by design — it is pinned to the dealership's allowed origins, not secret.

Request body
{
  "key": "oc_publishable_…",
  "widget": "oc-car-search"
}
Response 200
{
  "token": "string"
}
cURL
curl -X POST "https://api.car-search.ai/api/embed/exchange" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"oc_publishable_…","widget":"oc-car-search"}'

Legacy dealer API

Per-dealership `ddms_live_` keys — kept for back-compat. New partner integrations should use the `/v1` partner API above.

GET /api/v1/inventory

Requires the partner key only.

List active inventory

A `ddms_live_` key is scoped to a single dealership — there is no `{dealership_uid}` in the path.

NameInTypeDescription
offsetqueryinteger
limitqueryinteger
qquerystring
stock_statusquerystring
featuredquerystring (0 | 1)
Response 200
{
  "data": [
    {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 0,
    "total_results": 0,
    "total_pages": 0
  }
}
cURL
curl -X GET "https://api.car-search.ai/api/v1/inventory" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
GET /api/v1/inventory/{id}

Requires the partner key only.

Get a vehicle by UID or URL slug

NameInTypeDescription
id *pathstring
Response 200
{
  "data": {
    "vehicle": {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    },
    "images": [
      {}
    ]
  }
}
cURL
curl -X GET "https://api.car-search.ai/api/v1/inventory/PART_EXCHANGE_ID" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
GET /api/v1/recently-sold

Requires the partner key only.

Recently sold stock

NameInTypeDescription
daysqueryinteger
limitqueryinteger
Response 200
{
  "data": [
    {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "meta": {}
}
cURL
curl -X GET "https://api.car-search.ai/api/v1/recently-sold" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
GET /api/v1/sitemap

Requires the partner key only.

Sitemap feed

Response 200
{
  "data": [
    {}
  ],
  "meta": {}
}
cURL
curl -X GET "https://api.car-search.ai/api/v1/sitemap" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
GET /api/v1/dealership

Requires the partner key only.

Dealership profile (name, address, hours)

Response 200
{
  "data": {}
}
cURL
curl -X GET "https://api.car-search.ai/api/v1/dealership" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
POST /api/v1/leads

Requires the partner key only.

Capture a lead

Request body
{
  "name": "string",
  "email": "you@example.com",
  "phone": "string",
  "message": "string",
  "vehicle_uid": "00000000-0000-0000-0000-000000000000"
}
Response 201
{
  "ok": true,
  "lead_uid": "string",
  "customer_uid": "string"
}
cURL
curl -X POST "https://api.car-search.ai/api/v1/leads" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"string","email":"you@example.com","phone":"string","message":"string","vehicle_uid":"00000000-0000-0000-0000-000000000000"}'
POST /api/v1/search

Requires the partner key only.

AI-interpreted semantic search

Request body
{
  "query": "string",
  "filters": {},
  "offset": 0,
  "limit": 0
}
Response 200
{
  "data": [
    {
      "vehicle_uid": "00000000-0000-0000-0000-000000000000",
      "make": "string",
      "model": "string",
      "variant": "string",
      "model_year": 0,
      "mileage": 0,
      "price": 0,
      "currency_code": "string",
      "body_type": "string",
      "fuel_type": "string",
      "transmission": "string",
      "exterior_color": "string",
      "stock_status": "active",
      "url_slug": "string",
      "primary_image_uid": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "mode": "string",
  "pagination": {}
}
cURL
curl -X POST "https://api.car-search.ai/api/v1/search" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"string","filters":{},"offset":0,"limit":0}'
GET /api/v1/images/{imageId}/{size}

Requires the partner key only.

Serve a vehicle image

NameInTypeDescription
imageId *pathstring
size *pathstring (thumb | medium | full)
cURL
curl -X GET "https://api.car-search.ai/api/v1/images/IMAGEID/SIZE" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
GET /api/v1/widget/config

Requires the partner key only.

Chat widget branding

Response 200
{}
cURL
curl -X GET "https://api.car-search.ai/api/v1/widget/config" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY"
POST /api/v1/widget/chat

Requires the partner key only.

One chat turn

Request body
{
  "message": "string",
  "conversation_uid": "string",
  "visitor_id": "string",
  "history": [
    {}
  ]
}
Response 200
{}
cURL
curl -X POST "https://api.car-search.ai/api/v1/widget/chat" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"string","conversation_uid":"string","visitor_id":"string","history":[{}]}'
POST /api/v1/widget/chat/stream

Requires the partner key only.

Chat turn, streamed

Request body
{
  "message": "string"
}
cURL
curl -X POST "https://api.car-search.ai/api/v1/widget/chat/stream" \
  -H "Authorization: Bearer ddms_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"string"}'

Meta

GET /healthz

Requires the partner key only.

Health check

Response 200
{
  "ok": true
}
cURL
curl -X GET "https://api.car-search.ai/healthz" \
  -H "Authorization: Bearer csai_live_YOUR_PARTNER_KEY"