開發者 / API

Public API v0 · 只讀車源與庫存自動上傳

概覽

常來車網 Public API 提供只讀的車源與車行數據接口,以及車行庫存的自動上傳接口。個人用戶和車行使用同一套端點,只是寫入配額不同。

Base URLhttps://www.clcauto.com/api/v1
  • 請求與響應均為 JSON;字段名英文,枚舉字段返回 { value: 英文 slug, label: 按 lang 翻譯的文案 }。
  • 跨域開放(CORS *),可直接在瀏覽器或小程序裡調用只讀端點。
  • 列表端點用游標分頁:帶上返回的 next_cursor 取下一頁,limit 最大 100。

AI 格式

把下面這段說明整段複製,粘貼給 ChatGPT / Claude 等 AI 助手,再用一句話告訴它你要做什麼(例如「把這個 CSV 裡的車上傳到我的帳號」)。AI 就知道怎麼調接口、單位怎麼換算、什麼時候必須先 dry run。

約 10185 字

說明裡的 <API_KEY> 是佔位符:把你在「管理 API Key」創建的 key 單獨告訴 AI,不要貼到公開場合。

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
純文本地址(AI 也可直接抓取):https://www.clcauto.com/api/v1/llms.txt

鑑權

在門戶創建 API key,以 Bearer token 形式放在 Authorization 頭:

HTTP header
Authorization: Bearer clc_live_...
  • 只讀端點不帶 key 也能訪問,但按 IP 限流(每分鐘 20 次)且聯絡方式脫敏。
  • 帶 key 的請求按 key 自己的限速(預設每分鐘 60 次);寫入端點必須帶有對應 scope 的 key。
  • 每個帳號最多 5 個有效 key;key 明文只在創建時展示一次,泄露後請立即吊銷並重建。
read:cars讀取車源read:dealers讀取車行write:inventory寫入庫存write:images上傳圖片

配額檔位

檔位判定新上傳 / 天更改 / 天在售上限
個人預設1030與手動發帖一致
車行名下有已認證門店2000 次寫(合計)—500

配額按帳號計,同一帳號下所有 key 共享;每天 UTC 0 點重置。「新上傳」= 首次 PUT 某個 external_id;「更改」= 對已有車的 PUT / DELETE / 圖片上傳 / 對賬下架。每次寫入響應都帶 X-Quota-Inserts-Remaining / X-Quota-Updates-Remaining 頭,方便腳本自行節流。

端點一覽

方法路徑Scope說明
GET/cars—車源列表(游標分頁,支持篩選)
GET/cars/{id}—車源詳情
GET/dealers—已認證車行門店列表
GET/dealers/{id}—門店詳情與在售車
GET/meta/makes—品牌 / 車型 / 數量聚合
GET/meany當前 key、帳號、門店與配額
GET/inventorywrite:inventory我名下通過 API 管理的車
GET/inventory/{external_id}write:inventory按庫存編號查一條
PUT/inventory/{external_id}write:inventory冪等 upsert(核心端點)
DELETE/inventory/{external_id}write:inventory標記已售(預設)或下架
POST/inventory/{external_id}/imageswrite:images替換圖片列表
POST/inventory/batchwrite:inventory批量 upsert
POST/inventory/reconcilewrite:inventory全量對賬,不在列表裡的自動下架
GET/openapi.json—OpenAPI 3.1 規範

批量端點一次最多 100 條,逐條扣配額;超出部分逐條返回 quota_exceeded,前面已成功的不回滾。

車輛寫入字段

PUT /inventory/{external_id} 與 batch 的每條 item 共用同一套字段:

字段類型必填說明
external_idstring必填庫存編號,放在路徑裡;同一帳號內唯一
vinstring(17)必填同帳號 VIN 重複返回 409
yearinteger必填
make / modelstring必填英文品牌 / 車型
trimstring可選
priceinteger必填整數,幣種見 currency;0 = 電議
original_priceinteger可選
currencyenum可選USD(預設)、CAD、AUD 或 NZD;省略則保留原值,null 恢復 USD
mileageinteger必填整數,單位見 mileage_unit
mileage_unitenum可選mi(預設)或 km;省略則保留原值,null 恢復 mi
fuel_type / transmissionenum必填見下方枚舉表
drivetrainenum可選見下方枚舉表
title_statusenum可選產權狀態,見下方枚舉表;省略則保留原值,null 清空
conditionobject可選車況清單:service_records / one_owner / smoker(布爾)、keys_count(0–5)、tires_condition、known_issues(見下方枚舉表);不收自由文本,省略則保留原值,null 清空
exterior_color / interior_colorstring可選英文
descriptionstring可選任一語言,其他語言自動翻譯
description_langstring可選已棄用:可傳但會被忽略(語言自動檢測)
featuresstring[]可選
imagesstring[]可選https 連結,最多 30 張,單張 ≤ 10MB
carfax_urlstring可選
store_iduuid可選多門店時指定,省略用第一家已認證門店
locationstring個人必填城市或 zip;車行省略則跟隨門店地址

寫入同步落庫後立即返回;VIN 解碼、AI 審核、翻譯與圖片轉存在後台進行,響應裡的 processing 會列出仍在處理的步驟。圖片最多 30 張。

枚舉值

fuel_type燃料
gasoline汽油diesel柴油electric纯电hybrid混动phev插电混动hydrogen氢燃料
transmission變速箱
automaticAT 自动manualMT 手动cvtCVT 无级dctDCT 双离合amtAMT 机械自动single_speed单速(电动车)
drivetrain驅動
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悬挂
status狀態
activeactivependingpendingsoldsoldremovedremoved

示例

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 · 全量同步 + 對賬
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: { code, message, details? } };validation_failed 會在 details 裡逐字段說明。

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

觸發每分鐘限速返回 429 rate_limited(帶 Retry-After);當天寫配額用完返回 429 quota_exceeded,body 裡帶 quota:

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" }
  }
}
codeHTTP說明
bad_request / validation_failed400參數或請求體不合法
unauthorized / key_revoked / key_expired401缺少 key、key 無效、已吊銷或已過期
insufficient_scope / ip_not_allowed / forbidden403scope 不足、IP 不在白名單或帳號被封
not_found404資源不存在或不可見
conflict409同帳號 VIN 重複(附 existing_id,可加 ?adopt=true 接管)
listing_blocked409該車源已被系統或管理員下架,無法重新上架
rate_limited / quota_exceeded429限速或配額用盡
internal_error500服務端異常

規範由服務端生成,可直接導入 Postman / Scalar / 代碼生成器。