概覽
常來車網 Public API 提供只讀的車源與車行數據接口,以及車行庫存的自動上傳接口。個人用戶和車行使用同一套端點,只是寫入配額不同。
https://www.clcauto.com/api/v1- 請求與響應均為 JSON;字段名英文,枚舉字段返回 { value: 英文 slug, label: 按 lang 翻譯的文案 }。
- 跨域開放(CORS *),可直接在瀏覽器或小程序裡調用只讀端點。
- 列表端點用游標分頁:帶上返回的 next_cursor 取下一頁,limit 最大 100。
AI 格式
把下面這段說明整段複製,粘貼給 ChatGPT / Claude 等 AI 助手,再用一句話告訴它你要做什麼(例如「把這個 CSV 裡的車上傳到我的帳號」)。AI 就知道怎麼調接口、單位怎麼換算、什麼時候必須先 dry run。
說明裡的 <API_KEY> 是佔位符:把你在「管理 API Key」創建的 key 單獨告訴 AI,不要貼到公開場合。
# 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
鑑權
在門戶創建 API key,以 Bearer token 形式放在 Authorization 頭:
Authorization: Bearer clc_live_...- 只讀端點不帶 key 也能訪問,但按 IP 限流(每分鐘 20 次)且聯絡方式脫敏。
- 帶 key 的請求按 key 自己的限速(預設每分鐘 60 次);寫入端點必須帶有對應 scope 的 key。
- 每個帳號最多 5 個有效 key;key 明文只在創建時展示一次,泄露後請立即吊銷並重建。
配額檔位
| 檔位 | 判定 | 新上傳 / 天 | 更改 / 天 | 在售上限 |
|---|---|---|---|---|
| 個人 | 預設 | 10 | 30 | 與手動發帖一致 |
| 車行 | 名下有已認證門店 | 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 | /me | any | 當前 key、帳號、門店與配額 |
| GET | /inventory | write: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}/images | write:images | 替換圖片列表 |
| POST | /inventory/batch | write:inventory | 批量 upsert |
| POST | /inventory/reconcile | write:inventory | 全量對賬,不在列表裡的自動下架 |
| GET | /openapi.json | — | OpenAPI 3.1 規範 |
批量端點一次最多 100 條,逐條扣配額;超出部分逐條返回 quota_exceeded,前面已成功的不回滾。
車輛寫入字段
PUT /inventory/{external_id} 與 batch 的每條 item 共用同一套字段:
| 字段 | 類型 | 必填 | 說明 |
|---|---|---|---|
| external_id | string | 必填 | 庫存編號,放在路徑裡;同一帳號內唯一 |
| vin | string(17) | 必填 | 同帳號 VIN 重複返回 409 |
| year | integer | 必填 | |
| make / model | string | 必填 | 英文品牌 / 車型 |
| trim | string | 可選 | |
| price | integer | 必填 | 整數,幣種見 currency;0 = 電議 |
| original_price | integer | 可選 | |
| currency | enum | 可選 | USD(預設)、CAD、AUD 或 NZD;省略則保留原值,null 恢復 USD |
| mileage | integer | 必填 | 整數,單位見 mileage_unit |
| mileage_unit | enum | 可選 | mi(預設)或 km;省略則保留原值,null 恢復 mi |
| fuel_type / transmission | enum | 必填 | 見下方枚舉表 |
| drivetrain | enum | 可選 | 見下方枚舉表 |
| title_status | enum | 可選 | 產權狀態,見下方枚舉表;省略則保留原值,null 清空 |
| condition | object | 可選 | 車況清單:service_records / one_owner / smoker(布爾)、keys_count(0–5)、tires_condition、known_issues(見下方枚舉表);不收自由文本,省略則保留原值,null 清空 |
| exterior_color / interior_color | string | 可選 | 英文 |
| description | string | 可選 | 任一語言,其他語言自動翻譯 |
| description_lang | string | 可選 | 已棄用:可傳但會被忽略(語言自動檢測) |
| features | string[] | 可選 | |
| images | string[] | 可選 | https 連結,最多 30 張,單張 ≤ 10MB |
| carfax_url | string | 可選 | |
| store_id | uuid | 可選 | 多門店時指定,省略用第一家已認證門店 |
| location | string | 個人必填 | 城市或 zip;車行省略則跟隨門店地址 |
寫入同步落庫後立即返回;VIN 解碼、AI 審核、翻譯與圖片轉存在後台進行,響應裡的 processing 會列出仍在處理的步驟。圖片最多 30 張。
枚舉值
示例
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 "https://www.clcauto.com/api/v1/cars?make=Toyota&price_max=20000&sort=newest&limit=20&lang=en" \
-H "Authorization: Bearer clc_live_..."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 裡逐字段說明。
{
"error": {
"code": "validation_failed",
"message": "price is required",
"details": [{ "field": "price", "issue": "required" }]
}
}觸發每分鐘限速返回 429 rate_limited(帶 Retry-After);當天寫配額用完返回 429 quota_exceeded,body 裡帶 quota:
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" }
}
}| code | HTTP | 說明 |
|---|---|---|
| bad_request / validation_failed | 400 | 參數或請求體不合法 |
| unauthorized / key_revoked / key_expired | 401 | 缺少 key、key 無效、已吊銷或已過期 |
| insufficient_scope / ip_not_allowed / forbidden | 403 | scope 不足、IP 不在白名單或帳號被封 |
| not_found | 404 | 資源不存在或不可見 |
| conflict | 409 | 同帳號 VIN 重複(附 existing_id,可加 ?adopt=true 接管) |
| listing_blocked | 409 | 該車源已被系統或管理員下架,無法重新上架 |
| rate_limited / quota_exceeded | 429 | 限速或配額用盡 |
| internal_error | 500 | 服務端異常 |
規範由服務端生成,可直接導入 Postman / Scalar / 代碼生成器。