encarapi 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +130 -2
- package/package.json +50 -4
- package/src/china.js +50 -0
- package/src/http.js +65 -0
- package/src/index.d.ts +203 -0
- package/src/index.js +85 -0
- package/src/korea.js +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EnCarAPI (encarapi.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Note: This MIT license covers the client library only. Access to the EnCarAPI
|
|
24
|
+
service and its data requires a valid API key and is subject to the terms at
|
|
25
|
+
https://encarapi.com.
|
package/README.md
CHANGED
|
@@ -1,3 +1,131 @@
|
|
|
1
|
-
#
|
|
1
|
+
# EnCarAPI: Node.js client for Korean and Chinese used car data
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official **Node.js / TypeScript client** for [EnCarAPI](https://encarapi.com), the REST
|
|
4
|
+
**Encar API** and **Korean Car API**. One package for:
|
|
5
|
+
|
|
6
|
+
- **Korea:** Encar.com, KB Chachacha (`source: "kbc"`) and K Car (`source: "kcar"`), with
|
|
7
|
+
photos, specs, options, inspection reports, accident and ownership records, condition
|
|
8
|
+
reports, price history, a change feed and full catalog export.
|
|
9
|
+
- **China:** Dongchedi and Che168 in English via [ChinaCarAPI](https://chinacarapi.com)
|
|
10
|
+
(separate key or the China add-on for EnCarAPI keys).
|
|
11
|
+
|
|
12
|
+
Built for car exporters, dealers and marketplaces that need reliable used car data without
|
|
13
|
+
scraping, proxies or geo-blocks.
|
|
14
|
+
|
|
15
|
+
> **An API key is required.** The data is a paid service. Get a key (5-day trial) at
|
|
16
|
+
> **[encarapi.com](https://encarapi.com)**.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install encarapi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js 18+ (uses the global `fetch`). No dependencies. TypeScript types included.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const { EnCarAPI } = require("encarapi");
|
|
30
|
+
|
|
31
|
+
const client = new EnCarAPI("YOUR_API_KEY"); // or set ENCARAPI_KEY
|
|
32
|
+
|
|
33
|
+
// Search Korean listings with flat English filters
|
|
34
|
+
const cars = await client.korea.catalog({
|
|
35
|
+
manufacturer: "Hyundai",
|
|
36
|
+
max_mileage: 60000,
|
|
37
|
+
frame_clean: true, // accident-free chassis only
|
|
38
|
+
lang: "en",
|
|
39
|
+
count: true,
|
|
40
|
+
});
|
|
41
|
+
console.log(cars.Count, cars.SearchResults[0]);
|
|
42
|
+
|
|
43
|
+
// Full detail, inspection report and insurance record for one car
|
|
44
|
+
const detail = await client.korea.vehicle("12345678");
|
|
45
|
+
const inspection = await client.korea.inspection("12345678");
|
|
46
|
+
const record = await client.korea.record("12345678");
|
|
47
|
+
|
|
48
|
+
// KB Chachacha and K Car listings, or all three marketplaces deduplicated
|
|
49
|
+
const kbc = await client.korea.catalog({ source: "kbc", limit: 20 });
|
|
50
|
+
const all = await client.korea.catalog({ source: "all", count: true });
|
|
51
|
+
|
|
52
|
+
// Chinese listings (Dongchedi + Che168)
|
|
53
|
+
const byd = await client.china.catalog({ make: "BYD", export_ready: true, limit: 25 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Keep a local copy in sync (Business / Scale)
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// 1) Baseline: full catalog as CSV
|
|
60
|
+
const csv = await client.korea.exportCsv();
|
|
61
|
+
|
|
62
|
+
// 2) Then follow the change feed; every event arrives exactly once
|
|
63
|
+
for await (const change of client.korea.iterateChanges({ cursor: savedCursor ?? 0 })) {
|
|
64
|
+
// change.type: "added" | "updated" | "reappeared" | "removed"
|
|
65
|
+
}
|
|
66
|
+
saveCursor(client.korea.lastCursor);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Korea API (`client.korea`)
|
|
70
|
+
|
|
71
|
+
| Method | Endpoint | Notes |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `catalog(params)` | `GET /api/catalog` | Search & list. `source`: `encar` (default), `kbc`, `kcar`, `all` |
|
|
74
|
+
| `iterateCatalog(params)` | `GET /api/catalog` | Async iterator over all pages |
|
|
75
|
+
| `leasing(params)` | `GET /api/catalog/leasing` | Lease takeovers and rentals |
|
|
76
|
+
| `nav(params)` | `GET /api/nav` | Filter facets with counts |
|
|
77
|
+
| `enums(params)` | `GET /api/enums` | Valid values for the flat filters |
|
|
78
|
+
| `modelSearch(search, params)` | `GET /api/model-search` | Model autocomplete |
|
|
79
|
+
| `vehicle(id)` | `GET /api/vehicle/:id` | Ids: Encar id, `kbc:<id>`, `kcar:<id>` |
|
|
80
|
+
| `inspection(id)` | `GET /api/inspection/:id` | Official inspection report |
|
|
81
|
+
| `record(id)` | `GET /api/record/:id` | Accident & ownership record |
|
|
82
|
+
| `refresh(id)` | `POST /api/vehicle/:id/refresh` | On-demand refresh (daily quota) |
|
|
83
|
+
| `priceCheck(id)` | `GET /api/price-check/:id` | Market estimate (beta, Scale) |
|
|
84
|
+
| `bulkVehicles(ids)` / `bulkInspections(ids)` / `bulkRecords(ids)` | `POST /api/.../bulk` | Up to 500 ids (Business/Scale) |
|
|
85
|
+
| `inspectionVersions(id)` / `recordVersions(id)` | `GET /api/.../versions` | Report history (Business/Scale) |
|
|
86
|
+
| `changes(params)` / `iterateChanges(params)` | `GET /api/catalog/changes` | Incremental sync (Business/Scale) |
|
|
87
|
+
| `exportCsv()` | `GET /api/catalog/export` | Full catalog CSV (Business/Scale) |
|
|
88
|
+
| `makesCsv()` / `modelsCsv()` / `badgesCsv()` | `GET /api/taxonomy/*.csv` | Reference data |
|
|
89
|
+
|
|
90
|
+
## China API (`client.china`)
|
|
91
|
+
|
|
92
|
+
| Method | Endpoint | Notes |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `catalog(params)` | `GET /api/catalog` | `make`, `price_min/max` (CNY), `export_ready`, `sort`, ... |
|
|
95
|
+
| `vehicle(id)` | `GET /api/vehicle/:id` | Price history, photos, seller, export status |
|
|
96
|
+
| `inspection(id)` | `GET /api/inspection/:id` | Accident, flood and fire checks, EV battery data |
|
|
97
|
+
| `bulkVehicles(ids)` | `POST /api/vehicle/bulk` | Up to 500 ids |
|
|
98
|
+
| `changes(params)` | `GET /api/catalog/changes` | Change feed |
|
|
99
|
+
| `exportCsv()` | `GET /api/catalog/export` | Full catalog CSV |
|
|
100
|
+
| `enums()` / `models(make)` | `GET /api/enums`, `/api/models` | Filter values, models per make |
|
|
101
|
+
|
|
102
|
+
China only? `const { ChinaCarAPI } = require("encarapi")` (also available as the
|
|
103
|
+
[`chinacarapi`](https://www.npmjs.com/package/chinacarapi) package).
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
Every failed request throws `EnCarAPIError` with `status` and the raw `body`. A `403` on a
|
|
108
|
+
plan-gated endpoint includes an upgrade hint from the API.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
try {
|
|
112
|
+
await client.korea.changes({ cursor: 0 });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e.status === 403) console.log("Plan does not include this endpoint:", e.body);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Links
|
|
119
|
+
|
|
120
|
+
- Website and pricing: https://encarapi.com
|
|
121
|
+
- Documentation: https://encarapi.com/documentation
|
|
122
|
+
- OpenAPI reference: https://api.encarapi.com/reference
|
|
123
|
+
- China data: https://chinacarapi.com
|
|
124
|
+
- Python client: https://github.com/ThatMojo/encarapi-python
|
|
125
|
+
|
|
126
|
+
EnCarAPI is an independent service and not affiliated with Encar, KB Chachacha, K Car,
|
|
127
|
+
Dongchedi or Che168.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "encarapi",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Node.js client for EnCarAPI: Korean used car data (Encar, KB Chachacha, K Car) and Chinese used car data (Dongchedi, Che168) in one REST API.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"default": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node test/smoke.js",
|
|
19
|
+
"test:live": "node test/smoke.js --live"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"encar",
|
|
23
|
+
"encar api",
|
|
24
|
+
"encar.com",
|
|
25
|
+
"kbchacha",
|
|
26
|
+
"kb chachacha",
|
|
27
|
+
"kcar",
|
|
28
|
+
"k car",
|
|
29
|
+
"korean car api",
|
|
30
|
+
"korea car api",
|
|
31
|
+
"korean used car data",
|
|
32
|
+
"china car api",
|
|
33
|
+
"chinese used car data",
|
|
34
|
+
"che168",
|
|
35
|
+
"dongchedi",
|
|
36
|
+
"car export",
|
|
37
|
+
"vehicle data api"
|
|
38
|
+
],
|
|
39
|
+
"homepage": "https://encarapi.com",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/ThatMojo/encarapi-node.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/ThatMojo/encarapi-node/issues"
|
|
46
|
+
},
|
|
47
|
+
"author": "EnCarAPI (https://encarapi.com)",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/china.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { HttpClient } = require("./http");
|
|
4
|
+
|
|
5
|
+
const enc = encodeURIComponent;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Chinese used-car data (Dongchedi and Che168) in English. Works with a
|
|
9
|
+
* ChinaCarAPI key or an EnCarAPI key that has the China add-on.
|
|
10
|
+
*/
|
|
11
|
+
class ChinaClient {
|
|
12
|
+
constructor(apiKey, options = {}) {
|
|
13
|
+
this.http = new HttpClient({
|
|
14
|
+
apiKey,
|
|
15
|
+
baseUrl: options.baseUrl || "https://api.chinacarapi.com",
|
|
16
|
+
signupUrl: "https://chinacarapi.com",
|
|
17
|
+
product: "ChinaCarAPI",
|
|
18
|
+
fetchImpl: options.fetch,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Search listings. Filters: source, make, model, year_min/max, price_min/max (CNY), mileage_max, city, fuel, has_report, export_ready, sort, page, limit, lang. */
|
|
23
|
+
catalog(params) { return this.http.request("GET", "/api/catalog", { params }); }
|
|
24
|
+
|
|
25
|
+
/** Full record for one car: price history, photos, seller, export status, alsoListedOn. */
|
|
26
|
+
vehicle(id, params) { return this.http.request("GET", `/api/vehicle/${enc(id)}`, { params }); }
|
|
27
|
+
|
|
28
|
+
/** Inspection report: accident, flood and fire checks, battery data for EVs. */
|
|
29
|
+
inspection(id, params) { return this.http.request("GET", `/api/inspection/${enc(id)}`, { params }); }
|
|
30
|
+
|
|
31
|
+
/** Up to 500 full records in one call (Business/Scale). */
|
|
32
|
+
bulkVehicles(ids, params) { return this.http.request("POST", "/api/vehicle/bulk", { params, body: { ids } }); }
|
|
33
|
+
|
|
34
|
+
/** Alias of bulkVehicles (name used by the standalone chinacarapi 0.x client). */
|
|
35
|
+
bulk(ids, params) { return this.bulkVehicles(ids, params); }
|
|
36
|
+
|
|
37
|
+
/** Change feed: { since } once, then { cursor: nextCursor } (Business/Scale). */
|
|
38
|
+
changes(params) { return this.http.request("GET", "/api/catalog/changes", { params }); }
|
|
39
|
+
|
|
40
|
+
/** Full catalog as CSV text (Business/Scale). */
|
|
41
|
+
exportCsv(params) { return this.http.request("GET", "/api/catalog/export", { params, text: true }); }
|
|
42
|
+
|
|
43
|
+
/** Filter values with counts: makes, fuels, cities, sources, sorts. */
|
|
44
|
+
enums(params) { return this.http.request("GET", "/api/enums", { params }); }
|
|
45
|
+
|
|
46
|
+
/** Models of one make (id or English name) with counts. */
|
|
47
|
+
models(make) { return this.http.request("GET", "/api/models", { params: { make } }); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { ChinaClient };
|
package/src/http.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class EnCarAPIError extends Error {
|
|
4
|
+
constructor(message, status, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "EnCarAPIError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal fetch wrapper shared by the Korea and China clients. Sends the API key
|
|
14
|
+
* as `x-api-key`, drops undefined/null params, and turns 401/403 into an error
|
|
15
|
+
* that carries the server message (which includes upgrade hints for plan-gated
|
|
16
|
+
* endpoints) plus a link to the signup page.
|
|
17
|
+
*/
|
|
18
|
+
class HttpClient {
|
|
19
|
+
constructor({ apiKey, baseUrl, signupUrl, product, fetchImpl }) {
|
|
20
|
+
this.apiKey = apiKey;
|
|
21
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
22
|
+
this.signupUrl = signupUrl;
|
|
23
|
+
this.product = product;
|
|
24
|
+
this.fetch = fetchImpl || (typeof fetch === "function" ? fetch.bind(globalThis) : null);
|
|
25
|
+
if (!this.fetch) {
|
|
26
|
+
throw new EnCarAPIError("Global fetch is not available: Node.js 18+ is required (or pass options.fetch).");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async request(method, path, { params, body, text } = {}) {
|
|
31
|
+
const url = new URL(this.baseUrl + path);
|
|
32
|
+
for (const [k, v] of Object.entries(params || {})) {
|
|
33
|
+
if (v === undefined || v === null) continue;
|
|
34
|
+
url.searchParams.set(k, Array.isArray(v) ? v.join(",") : String(v));
|
|
35
|
+
}
|
|
36
|
+
const res = await this.fetch(url, {
|
|
37
|
+
method,
|
|
38
|
+
headers: {
|
|
39
|
+
"x-api-key": this.apiKey,
|
|
40
|
+
Accept: text ? "text/csv" : "application/json",
|
|
41
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
42
|
+
},
|
|
43
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
44
|
+
});
|
|
45
|
+
const raw = await res.text();
|
|
46
|
+
if (res.status === 401 || res.status === 403) {
|
|
47
|
+
throw new EnCarAPIError(
|
|
48
|
+
`${this.product} rejected the request (${res.status}). Check your key or plan at ${this.signupUrl}. Body: ${raw.slice(0, 300)}`,
|
|
49
|
+
res.status,
|
|
50
|
+
raw
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new EnCarAPIError(`${this.product} error ${res.status}: ${raw.slice(0, 300)}`, res.status, raw);
|
|
55
|
+
}
|
|
56
|
+
if (text) return raw;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { HttpClient, EnCarAPIError };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export interface ClientOptions {
|
|
2
|
+
/** Override the Korea API base URL (default https://api.encarapi.com). */
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
/** Override the China API base URL (default https://api.chinacarapi.com). */
|
|
5
|
+
chinaBaseUrl?: string;
|
|
6
|
+
/** Separate key for China data. Defaults to CHINACARAPI_KEY, then the main key. */
|
|
7
|
+
chinaKey?: string;
|
|
8
|
+
/** Custom fetch implementation (defaults to global fetch, Node.js 18+). */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class EnCarAPIError extends Error {
|
|
13
|
+
/** HTTP status code, if the error came from an API response. */
|
|
14
|
+
status?: number;
|
|
15
|
+
/** Raw response body, if any. */
|
|
16
|
+
body?: string;
|
|
17
|
+
}
|
|
18
|
+
export { EnCarAPIError as ChinaCarAPIError };
|
|
19
|
+
|
|
20
|
+
type Bool = boolean | "true" | "false";
|
|
21
|
+
type Json = Record<string, any>;
|
|
22
|
+
|
|
23
|
+
/** Korean data source. Encar is the default; "all" is the deduplicated union. */
|
|
24
|
+
export type KoreaSource = "encar" | "kbc" | "kcar" | "all" | (string & {});
|
|
25
|
+
|
|
26
|
+
/** Flat English filters for /api/catalog, /api/nav and /api/catalog/leasing. Values from korea.enums() / korea.nav(). */
|
|
27
|
+
export interface KoreaCatalogParams {
|
|
28
|
+
source?: KoreaSource;
|
|
29
|
+
manufacturer?: string;
|
|
30
|
+
model_group?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
badge_group?: string;
|
|
33
|
+
badge?: string;
|
|
34
|
+
model_search?: string;
|
|
35
|
+
fuel?: "gasoline" | "diesel" | "hybrid" | "diesel-hybrid" | "lpg-hybrid" | "gasoline-lpg" | "cng" | "electric" | "lpg" | "hydrogen" | "other";
|
|
36
|
+
transmission?: "automatic" | "manual" | "cvt" | "semi-automatic" | "other";
|
|
37
|
+
drivetrain?: string;
|
|
38
|
+
color?: string;
|
|
39
|
+
seat_color?: string;
|
|
40
|
+
category?: "suv" | "rv" | "large" | "midsize" | "compact" | "small" | "kei" | "van" | "light-van" | "truck" | "sports" | "other";
|
|
41
|
+
seats?: string | number;
|
|
42
|
+
min_seats?: number;
|
|
43
|
+
max_seats?: number;
|
|
44
|
+
/** Equipment codes; the car must have all of them. */
|
|
45
|
+
options?: string | string[];
|
|
46
|
+
/** Price in 만원 (1 = 10,000 KRW). */
|
|
47
|
+
min_price?: number;
|
|
48
|
+
max_price?: number;
|
|
49
|
+
min_year?: number;
|
|
50
|
+
max_year?: number;
|
|
51
|
+
min_mileage?: number;
|
|
52
|
+
max_mileage?: number;
|
|
53
|
+
min_ps?: number;
|
|
54
|
+
max_ps?: number;
|
|
55
|
+
max_owners?: number;
|
|
56
|
+
frame_clean?: Bool;
|
|
57
|
+
no_damage_cost?: Bool;
|
|
58
|
+
max_damage_eur?: number;
|
|
59
|
+
no_repairs?: Bool;
|
|
60
|
+
has_record?: Bool;
|
|
61
|
+
has_inspection?: Bool;
|
|
62
|
+
has_photos?: Bool;
|
|
63
|
+
usage_change?: string;
|
|
64
|
+
no_usage_change?: Bool;
|
|
65
|
+
exclude_prices?: string;
|
|
66
|
+
exclude_duplicates?: Bool;
|
|
67
|
+
min_first_seen?: string;
|
|
68
|
+
/** Raw Encar q-grammar (alternative to the flat filters). */
|
|
69
|
+
q?: string;
|
|
70
|
+
lang?: "en";
|
|
71
|
+
sort?: "newest" | "price_asc" | "price_desc" | "mileage_asc" | "mileage_desc" | "year_desc" | "year_asc";
|
|
72
|
+
page?: number;
|
|
73
|
+
limit?: number;
|
|
74
|
+
count?: Bool;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface KoreaCatalogResponse {
|
|
78
|
+
Count?: number;
|
|
79
|
+
SearchResults: Json[];
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface KoreaChangesParams {
|
|
84
|
+
cursor?: number;
|
|
85
|
+
since?: string;
|
|
86
|
+
source?: KoreaSource;
|
|
87
|
+
sell_type?: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
lang?: "en";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface KoreaChangesResponse {
|
|
93
|
+
nextCursor?: number;
|
|
94
|
+
hasMore?: boolean;
|
|
95
|
+
truncated?: boolean;
|
|
96
|
+
added?: Json[];
|
|
97
|
+
updated?: Json[];
|
|
98
|
+
reappeared?: Json[];
|
|
99
|
+
removed?: Json[];
|
|
100
|
+
[key: string]: any;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface KoreaChange extends Json {
|
|
104
|
+
type: "added" | "updated" | "reappeared" | "removed";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Korean used-car data: Encar, KB Chachacha ("kbc") and K Car ("kcar"). */
|
|
108
|
+
export class KoreaClient {
|
|
109
|
+
constructor(apiKey: string, options?: Pick<ClientOptions, "baseUrl" | "fetch">);
|
|
110
|
+
/** Cursor to resume from after iterateChanges() finished. */
|
|
111
|
+
lastCursor?: number;
|
|
112
|
+
catalog(params?: KoreaCatalogParams): Promise<KoreaCatalogResponse>;
|
|
113
|
+
leasing(params?: KoreaCatalogParams): Promise<KoreaCatalogResponse>;
|
|
114
|
+
nav(params?: KoreaCatalogParams): Promise<Json>;
|
|
115
|
+
enums(params?: { source?: KoreaSource }): Promise<Json>;
|
|
116
|
+
modelSearch(search: string, params?: { limit?: number; source?: KoreaSource; exclude_duplicates?: Bool; has_photos?: Bool; exclude_prices?: string; min_price?: number }): Promise<Json>;
|
|
117
|
+
iterateCatalog(params?: KoreaCatalogParams): AsyncGenerator<Json, void, unknown>;
|
|
118
|
+
/** Ids: Encar id, "kbc:<id>" or "kcar:<id>". */
|
|
119
|
+
vehicle(id: string, params?: { lang?: "en" }): Promise<Json>;
|
|
120
|
+
inspection(id: string): Promise<Json>;
|
|
121
|
+
record(id: string): Promise<Json>;
|
|
122
|
+
refresh(id: string): Promise<Json>;
|
|
123
|
+
/** Beta, Scale plan. */
|
|
124
|
+
priceCheck(id: string): Promise<Json>;
|
|
125
|
+
inspectionVersions(id: string): Promise<Json>;
|
|
126
|
+
recordVersions(id: string): Promise<Json>;
|
|
127
|
+
bulkVehicles(ids: string[]): Promise<Json>;
|
|
128
|
+
bulkInspections(ids: string[]): Promise<Json>;
|
|
129
|
+
bulkRecords(ids: string[]): Promise<Json>;
|
|
130
|
+
changes(params: KoreaChangesParams): Promise<KoreaChangesResponse>;
|
|
131
|
+
iterateChanges(params: KoreaChangesParams): AsyncGenerator<KoreaChange, void, unknown>;
|
|
132
|
+
/** CSV text. */
|
|
133
|
+
exportCsv(params?: { source?: KoreaSource }): Promise<string>;
|
|
134
|
+
makesCsv(params?: { source?: KoreaSource }): Promise<string>;
|
|
135
|
+
modelsCsv(params?: { source?: KoreaSource }): Promise<string>;
|
|
136
|
+
badgesCsv(params?: { source?: KoreaSource }): Promise<string>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ChinaCatalogParams {
|
|
140
|
+
source?: "dongchedi" | "che168" | (string & {});
|
|
141
|
+
duplicates?: "include" | (string & {});
|
|
142
|
+
make?: string;
|
|
143
|
+
model?: string;
|
|
144
|
+
year_min?: number;
|
|
145
|
+
year_max?: number;
|
|
146
|
+
reg_year_min?: number;
|
|
147
|
+
reg_year_max?: number;
|
|
148
|
+
/** Price in CNY. */
|
|
149
|
+
price_min?: number;
|
|
150
|
+
price_max?: number;
|
|
151
|
+
mileage_max?: number;
|
|
152
|
+
city?: string;
|
|
153
|
+
fuel?: string;
|
|
154
|
+
has_report?: Bool;
|
|
155
|
+
export_ready?: Bool;
|
|
156
|
+
updated_since?: string;
|
|
157
|
+
sort?: "newest" | "price_asc" | "price_desc" | "mileage_asc" | "year_desc";
|
|
158
|
+
page?: number;
|
|
159
|
+
limit?: number;
|
|
160
|
+
/** "zh" = original Chinese values. */
|
|
161
|
+
lang?: "en" | "zh";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ChinaCatalogResponse {
|
|
165
|
+
total: number;
|
|
166
|
+
page: number;
|
|
167
|
+
limit: number;
|
|
168
|
+
sort?: string;
|
|
169
|
+
results: Json[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Chinese used-car data: Dongchedi and Che168. */
|
|
173
|
+
export class ChinaClient {
|
|
174
|
+
constructor(apiKey: string, options?: Pick<ClientOptions, "fetch"> & { baseUrl?: string });
|
|
175
|
+
catalog(params?: ChinaCatalogParams): Promise<ChinaCatalogResponse>;
|
|
176
|
+
vehicle(id: string, params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
177
|
+
inspection(id: string, params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
178
|
+
bulkVehicles(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
179
|
+
/** Alias of bulkVehicles. */
|
|
180
|
+
bulk(ids: string[], params?: { lang?: "en" | "zh" }): Promise<Json>;
|
|
181
|
+
changes(params: { since?: string; cursor?: number; source?: string; limit?: number }): Promise<Json>;
|
|
182
|
+
exportCsv(params?: Json): Promise<string>;
|
|
183
|
+
enums(params?: Json): Promise<Json>;
|
|
184
|
+
models(make: string | number): Promise<Json>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One client for both markets. */
|
|
188
|
+
export class EnCarAPI {
|
|
189
|
+
constructor(apiKey?: string, options?: ClientOptions);
|
|
190
|
+
readonly korea: KoreaClient;
|
|
191
|
+
readonly china: ChinaClient;
|
|
192
|
+
/** Shortcut for korea.catalog (0.x compatible). */
|
|
193
|
+
catalog(params?: KoreaCatalogParams): Promise<KoreaCatalogResponse>;
|
|
194
|
+
/** Shortcut for korea.nav (0.x compatible). */
|
|
195
|
+
nav(params?: KoreaCatalogParams): Promise<Json>;
|
|
196
|
+
/** Shortcut for korea.vehicle (0.x compatible). */
|
|
197
|
+
vehicle(id: string, params?: { lang?: "en" }): Promise<Json>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** China-only client (also published as the `chinacarapi` package). */
|
|
201
|
+
export class ChinaCarAPI extends ChinaClient {
|
|
202
|
+
constructor(apiKey?: string, options?: Pick<ClientOptions, "fetch"> & { baseUrl?: string });
|
|
203
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { EnCarAPIError } = require("./http");
|
|
4
|
+
const { KoreaClient } = require("./korea");
|
|
5
|
+
const { ChinaClient } = require("./china");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Official Node.js client for EnCarAPI: Korean used-car data (Encar, KB Chachacha,
|
|
9
|
+
* K Car) and Chinese used-car data (Dongchedi, Che168) through one package.
|
|
10
|
+
*
|
|
11
|
+
* An API key is REQUIRED. Get one at https://encarapi.com (Korea) or
|
|
12
|
+
* https://chinacarapi.com (China). EnCarAPI keys with the China add-on work for both.
|
|
13
|
+
*
|
|
14
|
+
* const { EnCarAPI } = require("encarapi");
|
|
15
|
+
* const client = new EnCarAPI(process.env.ENCARAPI_KEY);
|
|
16
|
+
* const kr = await client.korea.catalog({ manufacturer: "Hyundai", lang: "en", count: true });
|
|
17
|
+
* const cn = await client.china.catalog({ make: "BYD", limit: 25 });
|
|
18
|
+
*/
|
|
19
|
+
class EnCarAPI {
|
|
20
|
+
constructor(apiKey, options = {}) {
|
|
21
|
+
apiKey = apiKey || process.env.ENCARAPI_KEY;
|
|
22
|
+
const chinaKey = options.chinaKey || process.env.CHINACARAPI_KEY || apiKey;
|
|
23
|
+
if (!apiKey && !chinaKey) {
|
|
24
|
+
throw new EnCarAPIError(
|
|
25
|
+
"An API key is required. Pass new EnCarAPI('YOUR_KEY') or set ENCARAPI_KEY " +
|
|
26
|
+
"(Korea) / CHINACARAPI_KEY (China). Get a key at https://encarapi.com"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
this._apiKey = apiKey;
|
|
30
|
+
this._chinaKey = chinaKey;
|
|
31
|
+
this._options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Korean data: Encar (default), KB Chachacha (source "kbc"), K Car (source "kcar"). */
|
|
35
|
+
get korea() {
|
|
36
|
+
if (!this._korea) {
|
|
37
|
+
if (!this._apiKey) {
|
|
38
|
+
throw new EnCarAPIError("An EnCarAPI key is required for Korean data. Get one at https://encarapi.com");
|
|
39
|
+
}
|
|
40
|
+
this._korea = new KoreaClient(this._apiKey, { baseUrl: this._options.baseUrl, fetch: this._options.fetch });
|
|
41
|
+
}
|
|
42
|
+
return this._korea;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Chinese data: Dongchedi and Che168 (ChinaCarAPI key or EnCarAPI key with the China add-on). */
|
|
46
|
+
get china() {
|
|
47
|
+
if (!this._china) {
|
|
48
|
+
this._china = new ChinaClient(this._chinaKey, { baseUrl: this._options.chinaBaseUrl, fetch: this._options.fetch });
|
|
49
|
+
}
|
|
50
|
+
return this._china;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Backwards compatible shortcuts from 0.x (Korean catalog).
|
|
54
|
+
catalog(params) { return this.korea.catalog(params); }
|
|
55
|
+
nav(params) { return this.korea.nav(params); }
|
|
56
|
+
vehicle(id, params) { return this.korea.vehicle(id, params); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* China-only entry point (also published as the `chinacarapi` package).
|
|
61
|
+
*
|
|
62
|
+
* const { ChinaCarAPI } = require("encarapi");
|
|
63
|
+
* const client = new ChinaCarAPI(process.env.CHINACARAPI_KEY);
|
|
64
|
+
*/
|
|
65
|
+
class ChinaCarAPI extends ChinaClient {
|
|
66
|
+
constructor(apiKey, options = {}) {
|
|
67
|
+
apiKey = apiKey || process.env.CHINACARAPI_KEY;
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
throw new EnCarAPIError(
|
|
70
|
+
"A ChinaCarAPI key is required. Pass new ChinaCarAPI('YOUR_KEY') or set CHINACARAPI_KEY. " +
|
|
71
|
+
"Get a key at https://chinacarapi.com"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
super(apiKey, options);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
EnCarAPI,
|
|
80
|
+
ChinaCarAPI,
|
|
81
|
+
KoreaClient,
|
|
82
|
+
ChinaClient,
|
|
83
|
+
EnCarAPIError,
|
|
84
|
+
ChinaCarAPIError: EnCarAPIError,
|
|
85
|
+
};
|
package/src/korea.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { HttpClient } = require("./http");
|
|
4
|
+
|
|
5
|
+
const enc = encodeURIComponent;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Korean used-car data: Encar plus KB Chachacha (`source: "kbc"`) and K Car
|
|
9
|
+
* (`source: "kcar"`). Encar is the default source; pass `source: "all"` for the
|
|
10
|
+
* deduplicated union. Endpoints marked Business/Scale return 403 with an upgrade
|
|
11
|
+
* hint on smaller plans.
|
|
12
|
+
*/
|
|
13
|
+
class KoreaClient {
|
|
14
|
+
constructor(apiKey, options = {}) {
|
|
15
|
+
this.http = new HttpClient({
|
|
16
|
+
apiKey,
|
|
17
|
+
baseUrl: options.baseUrl || "https://api.encarapi.com",
|
|
18
|
+
signupUrl: "https://encarapi.com",
|
|
19
|
+
product: "EnCarAPI",
|
|
20
|
+
fetchImpl: options.fetch,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// -- search ----------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Search & list vehicles. Flat English filters, e.g. { manufacturer: "BMW", max_mileage: 50000, lang: "en" }. */
|
|
27
|
+
catalog(params) { return this.http.request("GET", "/api/catalog", { params }); }
|
|
28
|
+
|
|
29
|
+
/** Lease-takeover and rental listings (same filters as catalog). */
|
|
30
|
+
leasing(params) { return this.http.request("GET", "/api/catalog/leasing", { params }); }
|
|
31
|
+
|
|
32
|
+
/** Filter facets with counts (the navigation tree). */
|
|
33
|
+
nav(params) { return this.http.request("GET", "/api/nav", { params }); }
|
|
34
|
+
|
|
35
|
+
/** Valid filter values for the flat parameters. */
|
|
36
|
+
enums(params) { return this.http.request("GET", "/api/enums", { params }); }
|
|
37
|
+
|
|
38
|
+
/** Model autocomplete across brands. */
|
|
39
|
+
modelSearch(search, params) { return this.http.request("GET", "/api/model-search", { params: { ...params, search } }); }
|
|
40
|
+
|
|
41
|
+
/** Async iterator over all catalog pages: for await (const car of korea.iterateCatalog({...})) {} */
|
|
42
|
+
async *iterateCatalog(params = {}) {
|
|
43
|
+
const limit = params.limit || 100;
|
|
44
|
+
for (let page = params.page || 1; ; page++) {
|
|
45
|
+
const res = await this.catalog({ ...params, limit, page });
|
|
46
|
+
const items = res.SearchResults || [];
|
|
47
|
+
for (const item of items) yield item;
|
|
48
|
+
if (items.length < limit) return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// -- one vehicle -------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/** Full vehicle detail. Ids: Encar id, "kbc:<id>" or "kcar:<id>". */
|
|
55
|
+
vehicle(id, params) { return this.http.request("GET", `/api/vehicle/${enc(id)}`, { params }); }
|
|
56
|
+
|
|
57
|
+
/** Official inspection report (성능점검). */
|
|
58
|
+
inspection(id) { return this.http.request("GET", `/api/inspection/${enc(id)}`); }
|
|
59
|
+
|
|
60
|
+
/** Insurance accident & ownership record. */
|
|
61
|
+
record(id) { return this.http.request("GET", `/api/record/${enc(id)}`); }
|
|
62
|
+
|
|
63
|
+
/** Ask for a fresh copy of one listing (daily quota per plan). */
|
|
64
|
+
refresh(id) { return this.http.request("POST", `/api/vehicle/${enc(id)}/refresh`); }
|
|
65
|
+
|
|
66
|
+
/** Price check (beta, Scale): below / in line with / above the market. */
|
|
67
|
+
priceCheck(id) { return this.http.request("GET", `/api/price-check/${enc(id)}`); }
|
|
68
|
+
|
|
69
|
+
/** Inspection report versions (Business/Scale). */
|
|
70
|
+
inspectionVersions(id) { return this.http.request("GET", `/api/inspection/${enc(id)}/versions`); }
|
|
71
|
+
|
|
72
|
+
/** Insurance record versions (Business/Scale). */
|
|
73
|
+
recordVersions(id) { return this.http.request("GET", `/api/record/${enc(id)}/versions`); }
|
|
74
|
+
|
|
75
|
+
// -- bulk & sync (Business/Scale) ---------------------------------------------
|
|
76
|
+
|
|
77
|
+
/** Up to 500 vehicle details in one call. */
|
|
78
|
+
bulkVehicles(ids) { return this.http.request("POST", "/api/vehicle/bulk", { body: { ids } }); }
|
|
79
|
+
|
|
80
|
+
/** Up to 500 inspection reports in one call. */
|
|
81
|
+
bulkInspections(ids) { return this.http.request("POST", "/api/inspection/bulk", { body: { ids } }); }
|
|
82
|
+
|
|
83
|
+
/** Up to 500 insurance records in one call. */
|
|
84
|
+
bulkRecords(ids) { return this.http.request("POST", "/api/record/bulk", { body: { ids } }); }
|
|
85
|
+
|
|
86
|
+
/** Incremental change feed: { cursor } from the previous call, or { since } right after an export. */
|
|
87
|
+
changes(params) { return this.http.request("GET", "/api/catalog/changes", { params }); }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Follows the change feed until it is drained and yields every change once as
|
|
91
|
+
* { type: "added" | "updated" | "reappeared" | "removed", ...item }. Pass
|
|
92
|
+
* { since } (right after an export) or { cursor } to start; after the loop,
|
|
93
|
+
* `korea.lastCursor` holds the cursor to resume from next time.
|
|
94
|
+
*/
|
|
95
|
+
async *iterateChanges(params = {}) {
|
|
96
|
+
let query = { ...params };
|
|
97
|
+
for (;;) {
|
|
98
|
+
const res = await this.changes(query);
|
|
99
|
+
for (const type of ["added", "updated", "reappeared", "removed"]) {
|
|
100
|
+
for (const item of res[type] || []) yield { type, ...item };
|
|
101
|
+
}
|
|
102
|
+
if (res.nextCursor !== undefined) this.lastCursor = res.nextCursor;
|
|
103
|
+
if (!(res.hasMore || res.truncated) || res.nextCursor === undefined) return;
|
|
104
|
+
query = { ...params, since: undefined, cursor: res.nextCursor };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Full catalog as CSV text. */
|
|
109
|
+
exportCsv(params) { return this.http.request("GET", "/api/catalog/export", { params, text: true }); }
|
|
110
|
+
|
|
111
|
+
// -- reference data -------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/** All makes as CSV. */
|
|
114
|
+
makesCsv(params) { return this.http.request("GET", "/api/taxonomy/makes.csv", { params, text: true }); }
|
|
115
|
+
|
|
116
|
+
/** All models as CSV. */
|
|
117
|
+
modelsCsv(params) { return this.http.request("GET", "/api/taxonomy/models.csv", { params, text: true }); }
|
|
118
|
+
|
|
119
|
+
/** All badges (trims) as CSV. */
|
|
120
|
+
badgesCsv(params) { return this.http.request("GET", "/api/taxonomy/badges.csv", { params, text: true }); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { KoreaClient };
|