開発者 / API

Public API v0 · read-only listings & inventory upload

Overview

The CLCAuto Public API exposes read-only listing and dealer data, plus endpoints for dealers to push inventory automatically. Individuals and dealers use the same endpoints; only the write quota differs.

Base URLhttps://www.clcauto.com/api/v1
  • Requests and responses are JSON. Field names are English; enum fields come back as { value: English slug, label: text translated by `lang` }.
  • CORS is open (`*`), so read endpoints can be called directly from browsers and mini-programs.
  • List endpoints use cursor pagination: pass the returned `next_cursor` for the next page; `limit` is capped at 100.

AI format

Copy the whole text below and paste it into ChatGPT, Claude or any AI assistant, then tell it what you want in plain words (e.g. "upload the cars in this CSV to my account"). It will know how to call the API, convert units, and when a dry run is required first.

~10185 chars

<API_KEY> in the text is a placeholder: give the AI the key you created under "Manage API keys" separately, and never paste it anywhere public.

llms.txt
# CLCAuto (常來車網) Public API — instructions for AI assistants

You are helping a user work with the CLCAuto Public API. CLCAuto is a non-profit, multilingual used-car marketplace in North America. Follow these rules exactly.

## Connection
- Base URL: https://www.clcauto.com/api/v1
- All requests and responses are JSON (UTF-8). CORS is open.
- Auth header: Authorization: Bearer <API_KEY>  (keys start with clc_live_). The user creates keys at https://www.clcauto.com/zh/developers/keys. Never print, log or echo the key back. If the user has not given you a key, use the literal placeholder <API_KEY> and ask them to fill it in.
- Read endpoints work without a key (20 requests/min per IP, contact info redacted). Write endpoints require a key with scope write:inventory (image endpoint: write:images).
- Optional query ?lang=zh|en|ko|ja|es|fr controls the "label" text of enum fields.

## Units and conventions
- price: integer in the listing's currency (0 = "call for price"); currency: USD (default) | CAD | AUD | NZD. mileage: integer in mileage_unit: mi (default) | km. year: model year.
- Responses always include currency and mileage_unit; price_min / price_max filters are USD and mileage_max is miles (non-USD / km listings are converted at fixed approximate rates before comparing).
- Enum fields are SENT as English slugs and RETURNED as { "value": slug, "label": translated text }:
  fuel_type: gasoline | diesel | electric | hybrid | phev | hydrogen
  transmission: automatic | manual | cvt | dct | amt | single_speed
  drivetrain: fwd | rwd | awd | 4wd
  title_status: clean | rebuilt | salvage | lemon_buyback | ppsr_clear | written_off
  condition.tires_condition: new | good | fair | needs_replacement
  condition.known_issues (array): engine | transmission | air_conditioning | electrical | oil_leak | rust | accident_repair | suspension
- Lists are cursor-paginated: pass cursor = previous next_cursor; limit ≤ 100 (default 20).
- Error body: { "error": { "code": string, "message": string, "details"?: [{ "field": string, "issue": string }] } }
  400 bad_request / validation_failed · 401 unauthorized / key_revoked / key_expired · 403 insufficient_scope / ip_not_allowed / forbidden / account_banned · 404 not_found · 409 conflict / listing_blocked · 429 rate_limited / quota_exceeded · 500 internal_error
- On 429 rate_limited: wait Retry-After seconds, then retry once. On 429 quota_exceeded: STOP; body.quota.resets_at (UTC midnight) says when to continue. Never retry in a tight loop.
- On 409 listing_blocked: the listing was removed by moderation or an administrator and cannot be relisted. Do not retry; tell the user.

## Quotas (per account per UTC day, shared by all of the account's keys)
- personal tier: 10 new listings, 30 changes per day.
- dealer tier (account has a verified store): 2000 writes per day total, max 500 active listings.
- Every write response includes a "quota" object and headers X-Quota-Inserts-Remaining / X-Quota-Updates-Remaining. GET /me shows the current tier and quota.

## Endpoints
GET    /cars                          filters: make, model, year_min, year_max, price_min, price_max, mileage_max, fuel_type, transmission, drivetrain, location, seller_id (key required), updated_since (ISO 8601; listings whose visible content changed since then — compare with updated_at), sort (newest|price_asc|price_desc|mileage_asc|year_desc), limit, cursor, lang
GET    /cars/{id}                     description / features in lang once translated (description_translated tells you); description_original and features_original are the seller's own text, description_lang its ISO 639-1 language (null when not detected or undeterminable)
GET    /dealers                       filters: location, limit, cursor
GET    /dealers/{id}                  store + up to 50 active cars
GET    /meta/makes                    make → models → counts, plus the full enum catalog
GET    /me                            key, owner, tier, verified stores, quota
GET    /inventory                     the account's own listings (all statuses); filters: status, external_id, limit, cursor
GET    /inventory/{external_id}
PUT    /inventory/{external_id}       idempotent create-or-update → 201 created / 200 updated. After a 409 conflict with existing_id (same VIN already posted manually) you may re-send with ?adopt=true to attach external_id to that listing.
DELETE /inventory/{external_id}       ?reason=sold (default) | removed
POST   /inventory/batch               { "items": [ListingInput, ...] } ≤ 100 items; per-item results; earlier successes are kept when a later item fails
POST   /inventory/reconcile           { "active_external_ids": [...], "dry_run": true|false } — every listing NOT in the list is marked removed. ALWAYS call with dry_run:true first and show the user what would be removed. An empty list additionally requires "confirm_empty": true.
POST   /inventory/{external_id}/images  { "images": ["https://...", ...] } ≤ 30, https only, first image = cover

## ListingInput (body of PUT, and each item of /inventory/batch)
Required: vin (17 chars, no I/O/Q), year, make, model, price, mileage, fuel_type, transmission.
Optional: trim, original_price, currency (USD|CAD|AUD|NZD), mileage_unit (mi|km), country (US|CA|AU|NZ), drivetrain, title_status, condition, exterior_color, interior_color, description, features (string[] ≤ 40), images (https URLs ≤ 30), carfax_url, store_id (dealers with several stores), location.
- external_id is the dealer's own stock number and goes in the URL path (in batch items it is a body field).
- currency / mileage_unit: send currency "CAD" / "AUD" / "NZD" for Canadian / Australian / New Zealand dollar prices and mileage_unit "km" for odometer readings in kilometres, with the numbers exactly as the user gave them. Omitting them keeps the stored value (new listings default to USD / mi); null resets to the default.
- title_status: only send it when the user states the title brand (clean title, rebuilt, salvage, lemon buyback) — never infer it from "no accidents" or a Carfax report. For Australian / New Zealand cars use ppsr_clear (the seller checked the official PPSR: no money owing) or written_off (recorded as an insurance write-off) instead — only when the user says so. Omitting it keeps the stored value; null clears it.
- condition: optional checklist object { service_records: bool, one_owner: bool, smoker: bool, keys_count: 0-5, tires_condition, known_issues: [slugs] }. Every item is optional — only include what the user actually states; never infer. known_issues [] means "no known issues" (only when the user says so). There is no free-text key (unknown keys → 400); put details in description. Omitting condition keeps the stored checklist; null clears it; an object replaces the whole checklist.
- location: dealer-tier accounts may omit it (the verified store address is used). Personal accounts MUST send it as "City, ST", a 5-digit US zip, a Canadian postal code (A1A 1A1) or a 4-digit Australian / New Zealand postcode.
- country: optional US / CA / AU / NZ. Required with a 4-digit postcode (Australia and New Zealand both use 4 digits) and must match the postcode format. On a new listing it sets the default currency / mileage_unit when you omit them (AU → AUD / km, NZ → NZD / km, CA → CAD / km, US → USD / mi). Responses carry a derived country field (inferred from the coordinates / location, not stored).
- description and features: send the user's own wording in any language — do NOT translate them yourself. They are stored verbatim and machine-translated into the other site languages. description_lang is deprecated and ignored (the language is detected automatically); omit it.
- The server decodes the VIN, mirrors the images, runs content moderation and translates asynchronously. The response "processing" array lists what is still running (vin, moderation, translation, images) — do not poll for it; GET /inventory later shows processing_state (images_pending, translation_pending).
- Re-sending a listing whose description, features, year, make, model and trim are unchanged triggers no new moderation, and translation only re-runs when the description changes, so periodic full syncs are cheap.
- Listings never expire; they stay live until you DELETE them, mark them sold, or reconcile them away.

Example
PUT https://www.clcauto.com/api/v1/inventory/STK12345
{
  "vin": "1HGCV1F3XLA012345", "year": 2020, "make": "Honda", "model": "Accord", "trim": "Sport",
  "price": 18900, "mileage": 41200, "fuel_type": "gasoline", "transmission": "cvt", "drivetrain": "fwd",
  "exterior_color": "white", "description": "One owner, clean title, dealer maintained.",
  "images": ["https://cdn.example.com/stk12345/1.jpg", "https://cdn.example.com/stk12345/2.jpg"]
}
→ 201 { "id": "…", "external_id": "STK12345", "url": "https://www.clcauto.com/zh/car/…", "status": "active", "created": true, "processing": ["vin", "moderation", "translation", "images"], "quota": { … } }

## Rules for you, the assistant
1. Units: never convert between currencies (USD/CAD/AUD/NZD) or miles/km yourself — send the figure as given with the matching currency / mileage_unit. Prices in other currencies (RMB etc.): ask the user for the figure in one of the supported currencies (USD, CAD, AUD, NZD); never guess a price.
2. Validate locally before calling: VIN is 17 characters, all required fields present, enum slugs from the lists above. Ask the user for anything missing instead of inventing values.
3. Importing a CSV or spreadsheet: map the columns to ListingInput, show the user 1–2 mapped rows and ask for confirmation, then send /inventory/batch in chunks of ≤ 100.
4. Full inventory sync: batch-upsert everything, then /inventory/reconcile with dry_run:true, show the would_remove list, and only run dry_run:false after the user explicitly confirms.
5. After every write, summarize for the user: created / updated / failed counts, remaining quota, and any validation_failed details verbatim (field + issue).
6. Only use the endpoints listed here. Machine-readable spec: https://www.clcauto.com/api/v1/openapi.json — this document: https://www.clcauto.com/api/v1/llms.txt
Plain-text URL (AI agents can fetch it directly):https://www.clcauto.com/api/v1/llms.txt

Authentication

Create an API key in the portal and send it as a bearer token in the Authorization header:

HTTP header
Authorization: Bearer clc_live_...
  • Read endpoints work without a key, but are limited to 20 requests/min per IP and contact details are masked.
  • Requests with a key use the key's own rate limit (default 60/min). Write endpoints always require a key with the matching scope.
  • Up to 5 active keys per account. The plaintext is shown only once at creation; revoke and recreate immediately if it leaks.
read:carsRead listingsread:dealersRead dealerswrite:inventoryWrite inventorywrite:imagesUpload images

Quota tiers

TierConditionNew listings / dayUpdates / dayActive cap
個人Default1030Same as manual posting
ディーラーHas a verified store2000 writes (combined)—500

Quota is per account and shared by all its keys; it resets at 00:00 UTC. An "insert" is the first PUT of an external_id; an "update" is any PUT / DELETE / image upload / reconcile removal on an existing listing. Every write response carries X-Quota-Inserts-Remaining / X-Quota-Updates-Remaining so scripts can throttle themselves.

Endpoints

MethodPathScopeDescription
GET/cars—List listings (cursor-paginated, filterable)
GET/cars/{id}—Listing detail
GET/dealers—Verified dealer stores
GET/dealers/{id}—Store detail with active listings
GET/meta/makes—Make / model / count aggregates
GET/meanyCurrent key, account, stores and quota
GET/inventorywrite:inventoryMy API-managed listings
GET/inventory/{external_id}write:inventoryGet one by stock number
PUT/inventory/{external_id}write:inventoryIdempotent upsert (core endpoint)
DELETE/inventory/{external_id}write:inventoryMark sold (default) or removed
POST/inventory/{external_id}/imageswrite:imagesReplace image list
POST/inventory/batchwrite:inventoryBatch upsert
POST/inventory/reconcilewrite:inventoryFull reconcile; anything not listed is removed
GET/openapi.json—OpenAPI 3.1 spec

Batch endpoints take up to 100 items and charge quota per item; items past the limit come back as quota_exceeded while earlier ones stay written.

Listing fields

PUT /inventory/{external_id} and each batch item share the same fields:

FieldTypeRequiredDescription
external_idstringRequiredStock number, in the path; unique per account
vinstring(17)RequiredDuplicate VIN under the same account returns 409
yearintegerRequired
make / modelstringRequiredMake / model in English
trimstringOptional
priceintegerRequired整数、通貨は currency を参照;0 = 価格応談
original_priceintegerOptional
currencyenumOptionalUSD(既定)、CAD、AUD または NZD;省略すると既存値を保持、null で USD に戻す
mileageintegerRequired整数、単位は mileage_unit を参照
mileage_unitenumOptionalmi(既定)または km;省略すると既存値を保持、null で mi に戻す
fuel_type / transmissionenumRequiredSee enum table below
drivetrainenumOptionalSee enum table below
title_statusenumOptionalタイトルの状態。下の列挙表を参照。省略すると現在の値を保持し、null でクリア
conditionobjectOptional車両状態チェックリスト:service_records / one_owner / smoker(真偽値)、keys_count(0–5)、tires_condition、known_issues(下の列挙値表を参照)。自由記述不可。省略すると現在の値を保持、null でクリア
exterior_color / interior_colorstringOptionalEnglish
descriptionstringOptionalAny language; others are translated automatically
description_langstringOptionalDeprecated: accepted but ignored (language is detected automatically)
featuresstring[]Optional
imagesstring[]Optionalhttps URLs, max 30, each ≤ 10 MB
carfax_urlstringOptional
store_iduuidOptionalPick a store when you have several; defaults to the first verified store
locationstringRequired for individualsCity or ZIP; dealers default to the store address

Writes return as soon as the row is saved; VIN decoding, AI moderation, translation and image mirroring run in the background and `processing` lists the steps still pending. Up to 30 images.

Enum values

fuel_typeFuel
gasolineガソリンdieselディーゼルelectricEVhybridハイブリッドphevプラグインハイブリッドhydrogen水素燃料電池
transmissionトランスミッション
automaticAT(オートマ)manualMT(マニュアル)cvtCVTdctDCT(デュアルクラッチ)amtAMT(セミオートマ)single_speedシングルスピード(EV)
drivetrainDrivetrain
fwdFWDrwdRWDawdAWD4wd4WD
title_statusタイトルの状態
cleanクリーンタイトルrebuiltリビルトタイトルsalvageサルベージタイトルlemon_buybackレモン法買い戻し車ppsr_clear残債なし(PPSR確認済み)written_off全損歴あり(written-off)
tires_conditionタイヤの状態
new新品good良好fair普通needs_replacement要交換
known_issue既知の不具合
engineエンジンtransmissionトランスミッションair_conditioningエアコンelectrical電装系oil_leakオイル漏れrustボディの錆accident_repair事故修復suspensionサスペンション
statusStatus
activeactivependingpendingsoldsoldremovedremoved

Examples

curl · PUT /inventory/{external_id}
curl -X PUT https://www.clcauto.com/api/v1/inventory/STK12345 \
  -H "Authorization: Bearer clc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "vin": "1HGCV1F3XLA012345",
    "year": 2020, "make": "Honda", "model": "Accord", "trim": "Sport",
    "price": 18900, "mileage": 41200,
    "fuel_type": "gasoline", "transmission": "cvt", "drivetrain": "fwd",
    "exterior_color": "white", "interior_color": "black",
    "description": "One owner, clean title.",
    "features": ["Apple CarPlay", "Sunroof"],
    "images": ["https://cdn.example.com/stk12345/1.jpg"]
  }'
curl · GET /cars
curl "https://www.clcauto.com/api/v1/cars?make=Toyota&price_max=20000&sort=newest&limit=20&lang=en" \
  -H "Authorization: Bearer clc_live_..."
Node.js · full sync + reconcile
const BASE = "https://www.clcauto.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.CLC_API_KEY}`,
  "Content-Type": "application/json",
};

// 1. Current stock exported from your DMS
//    [{ external_id, vin, year, make, model, price, mileage, images, ... }]
const stock = await loadStockFromDms();

// 2. Upsert in batches of 100
for (let i = 0; i < stock.length; i += 100) {
  const res = await fetch(`${BASE}/inventory/batch`, {
    method: "POST",
    headers,
    body: JSON.stringify({ items: stock.slice(i, i + 100) }),
  });
  const { results } = await res.json();
  for (const r of results) if (!r.ok) console.warn(r.external_id, r.error);
}

// 3. Reconcile: anything not in the list is marked removed
const rec = await fetch(`${BASE}/inventory/reconcile`, {
  method: "POST",
  headers,
  body: JSON.stringify({ active_external_ids: stock.map((s) => s.external_id) }),
});
console.log(await rec.json()); // { dry_run: false, removed: [{ id, external_id }], failed: [], quota }

Error format

Every error is { error: { code, message, details? } }; validation_failed lists each offending field in details.

400 validation_failed
{
  "error": {
    "code": "validation_failed",
    "message": "price is required",
    "details": [{ "field": "price", "issue": "required" }]
  }
}

Exceeding the per-minute limit returns 429 rate_limited (with Retry-After); using up the daily write quota returns 429 quota_exceeded with `quota` in the body:

429 quota_exceeded
HTTP/1.1 429 Too Many Requests
X-Quota-Tier: personal
X-Quota-Inserts-Remaining: 0
X-Quota-Updates-Remaining: 12

{
  "error": {
    "code": "quota_exceeded",
    "message": "Daily insert quota reached",
    "quota": { "tier": "personal", "inserts_used": 10, "inserts_limit": 10,
               "updates_used": 18, "updates_limit": 30, "resets_at": "2026-09-03T00:00:00.000Z" }
  }
}
codeHTTPDescription
bad_request / validation_failed400Invalid parameters or body
unauthorized / key_revoked / key_expired401Missing, invalid, revoked or expired key
insufficient_scope / ip_not_allowed / forbidden403Insufficient scope, IP not allowed, or account banned
not_found404Resource missing or not visible
conflict409Duplicate VIN under this account (includes existing_id; retry with ?adopt=true to attach)
listing_blocked409この掲載は審査または管理者により停止されたため、再掲載できません。
rate_limited / quota_exceeded429Rate limit or quota exhausted
internal_error500Server error

Generated server-side; import it straight into Postman, Scalar or a code generator.