概览
常來車網 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 / 代码生成器。