Korea Import API
The Korea Import API serves your Korean cars, at your prices, as JSON for your own website. You get a key, you call one URL, and you render the cars however your site already looks — no iframe, no framework, no Scriptiflow branding.
This API is part of the Korea Import add-on. The cars it returns are the ones you publish from Korea Import, priced by your pricing rules and any per-car overrides.
This page is written for whoever builds your website. Everything below works with plain HTML and JavaScript.
Getting a key
Open the Public API panel
Go to Settings → Integrations → Public API.
Create a key
Click Create a key and give it a name you’ll recognise later — Website or
Dealer site is enough.
Copy the key now
The full key (sf_pk_live_…) is shown once, right after you create it. Copy
it into your site’s configuration before closing the dialog. Afterwards the
panel only shows the first few characters, so you can tell keys apart.
Turn the feed on
The feed only answers while Public API is switched on for the brand. If it’s
off, every request comes back as 503.
If you lose a key, you can’t recover it — create a new one, swap it into your
site, then revoke the old one. Revoking takes effect immediately: any site
still using that key starts getting 401.
A key belongs to one brand and returns that brand’s cars only. If you run several rooftops, create one key per brand.
Authentication
Send the key as a header on every request:
GET /api/public/v1/vehicles HTTP/1.1
Host: api.scriptiflow.com
X-Scriptiflow-Key: sf_pk_live_a1b2c3d4e5f6For embeds where you genuinely can’t set a header, the key can also go in the query string:
https://api.scriptiflow.com/api/public/v1/vehicles?key=sf_pk_live_a1b2c3d4e5f6A key in a URL ends up in server logs, browser history, analytics and
Referer headers. Use the header wherever you can, and treat a ?key= key as
something you’ll rotate more often.
Base URL
https://api.scriptiflow.com/api/public/v1All responses are JSON, UTF-8, and all prices are integers in the brand’s
currency (EUR unless you changed it).
Endpoints
GET /vehicles
The list. Returns published cars with paging.
GET /api/public/v1/vehicles?make=BMW&year_from=2018&sort=price_asc&page=1&limit=24| Parameter | Values | Default | Notes |
|---|---|---|---|
scope | selected, all | your brand’s setting | selected = only the cars you picked. all = everything your brand publishes. Your brand’s setting is the ceiling — asking for all never returns more than you’ve chosen to publish. |
make | exact make, e.g. BMW | — | Use a value from /filters. |
model | exact model, e.g. 520d | — | Use a value from /filters. |
year_from | year, e.g. 2018 | — | Inclusive. |
year_to | year, e.g. 2022 | — | Inclusive. |
fuel | e.g. Diesel | — | Use a value from /filters. |
gearbox | e.g. Automatic | — | Use a value from /filters. |
body | e.g. SUV | — | Use a value from /filters. |
price_from | whole units, e.g. 15000 | — | Matched against the price in the response, not any internal figure. |
price_to | whole units, e.g. 40000 | — | Same. |
km_to | kilometres, e.g. 120000 | — | Maximum mileage. |
sort | new, price_asc, price_desc, km, year | new | new = most recently added first. km = lowest mileage first. year = newest model year first. |
page | 1 and up | 1 | 1-based. |
limit | 1–100 | 24 | Cars per page. |
Unknown parameters are ignored, so you can add your own cache-busting values without breaking anything.
GET /vehicles/:id
One car, same fields as a list row.
GET /api/public/v1/vehicles/b0c1d2e3-4f56-7890-abcd-ef0123456789Returns 404 if the car isn’t published by your brand any more — a sold car
disappears from the feed, so handle 404 on your detail pages and redirect back
to the list.
GET /filters
The values that actually exist in your feed right now, with counts. Use it to build your filter dropdowns instead of hard-coding makes.
{
"makes": [{ "value": "Hyundai", "count": 214 }, { "value": "BMW", "count": 96 }],
"models": [{ "value": "520d", "count": 12 }],
"fuels": [{ "value": "Diesel", "count": 180 }, { "value": "Petrol", "count": 142 }],
"bodies": [{ "value": "SUV", "count": 160 }, { "value": "Sedan", "count": 122 }],
"years": { "min": 2016, "max": 2024 }
}models is empty until you pass ?make= — models are returned for one make at a
time, because the full list is thousands of entries.
Example response
{
"data": [
{
"id": "b0c1d2e3-4f56-7890-abcd-ef0123456789",
"make": "BMW",
"model": "520d",
"generation": "G30",
"year": 2019,
"mileage_km": 78400,
"fuel": "Diesel",
"gearbox": "Automatic",
"body_type": "Sedan",
"color": "Black",
"power_ps": 190,
"price": 24950,
"currency": "EUR",
"price_display": "24.950 €",
"price_note": "excl. customs",
"images": [
"https://cdn.scriptiflow.com/korea/b0c1d2e3/01.jpg",
"https://cdn.scriptiflow.com/korea/b0c1d2e3/02.jpg"
],
"image": "https://cdn.scriptiflow.com/korea/b0c1d2e3/01.jpg",
"source": "encar"
}
],
"meta": { "page": 1, "limit": 24, "total": 312, "has_more": true }
}A few fields are worth knowing:
| Field | What it is |
|---|---|
id | A stable, non-guessable ID. Use it in your own URLs and pass it to /vehicles/:id. It stays the same for the life of the car. |
price | A number, for sorting and filtering on your side. null means price on request. |
price_display | The same price already formatted for the brand’s market. Render this one — it saves you writing currency formatting. |
price_note | Optional line the dealer sets, e.g. excl. customs. May be null; show it next to the price when present. |
image | The first image, so you don’t have to index into images. images is the full ordered list. |
source | Where the car came from. Currently always encar. |
Fields that don’t apply to a car are null rather than missing, so you can read
them without guarding every access.
Copy-paste: fetch
const SF_KEY = 'sf_pk_live_a1b2c3d4e5f6';
const SF_API = 'https://api.scriptiflow.com/api/public/v1';
async function loadVehicles(params = {}) {
const qs = new URLSearchParams({ limit: '24', ...params });
const res = await fetch(`${SF_API}/vehicles?${qs}`, {
headers: { 'X-Scriptiflow-Key': SF_KEY }
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') || 5);
throw new Error(`Rate limited. Retry in ${retryAfter}s.`);
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `Request failed with status ${res.status}`);
}
const { data, meta } = await res.json();
return { vehicles: data, meta };
}
async function main() {
try {
const { vehicles, meta } = await loadVehicles({
make: 'BMW',
year_from: '2018',
sort: 'price_asc'
});
console.log(`Showing ${vehicles.length} of ${meta.total} cars`);
} catch (err) {
console.error('Could not load vehicles:', err.message);
}
}
main();Copy-paste: plain HTML embed
Paste this where you want the cars to appear, replace the key, and you’re done. No build step, no dependencies, works in any CMS block that allows HTML.
<div id="sf-cars"></div>
<script>
(function () {
var API = 'https://api.scriptiflow.com/api/public/v1/vehicles';
var KEY = 'sf_pk_live_a1b2c3d4e5f6'; // <- your key
var LIMIT = 24;
var mount = document.getElementById('sf-cars');
if (!mount) return;
var style = document.createElement('style');
style.textContent = [
'#sf-cars{display:grid;gap:16px;font-family:system-ui,-apple-system,sans-serif;',
'grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}',
'#sf-cars .sf-card{border:1px solid #e5e5e5;border-radius:8px;overflow:hidden;background:#fff}',
'#sf-cars .sf-card img{display:block;width:100%;height:170px;object-fit:cover;background:#f2f2f2}',
'#sf-cars .sf-body{padding:12px}',
'#sf-cars .sf-title{margin:0 0 4px;font-size:15px;font-weight:600}',
'#sf-cars .sf-meta{margin:0 0 8px;font-size:13px;color:#666}',
'#sf-cars .sf-price{font-size:16px;font-weight:600}',
'#sf-cars .sf-note{font-size:12px;font-weight:400;color:#666}',
'#sf-cars .sf-msg{grid-column:1/-1;font-size:14px;color:#666}'
].join('');
document.head.appendChild(style);
var ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return ESCAPES[c];
});
}
function km(value) {
return typeof value === 'number' ? value.toLocaleString('de-DE') + ' km' : '';
}
function card(car) {
var title = [car.make, car.model].filter(Boolean).join(' ');
var meta = [car.year, km(car.mileage_km), car.fuel, car.gearbox].filter(Boolean).join(' · ');
var note = car.price_note
? ' <span class="sf-note">' + esc(car.price_note) + '</span>'
: '';
var image = car.image
? '<img src="' + esc(car.image) + '" alt="' + esc(title) + '" loading="lazy">'
: '';
return '<article class="sf-card">' + image +
'<div class="sf-body">' +
'<p class="sf-title">' + esc(title) + '</p>' +
'<p class="sf-meta">' + esc(meta) + '</p>' +
'<div class="sf-price">' + esc(car.price_display || 'Price on request') + note + '</div>' +
'</div>' +
'</article>';
}
mount.innerHTML = '<p class="sf-msg">Loading cars…</p>';
fetch(API + '?limit=' + LIMIT, { headers: { 'X-Scriptiflow-Key': KEY } })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
})
.then(function (body) {
var cars = (body && body.data) || [];
mount.innerHTML = cars.length
? cars.map(card).join('')
: '<p class="sf-msg">No cars available right now.</p>';
})
.catch(function (err) {
mount.innerHTML = '<p class="sf-msg">Cars are unavailable right now.</p>';
console.error('[scriptiflow]', err);
});
})();
</script>To link each card to your own detail page, build the URL from car.id inside
card():
var href = '/cars/' + encodeURIComponent(car.id);
return '<a class="sf-card" href="' + href + '">' + image + '…</a>';A browser request that sends the X-Scriptiflow-Key header triggers a CORS
preflight. If you’ve set a domain allowlist on the key, add the exact origin
your site is served from (including https:// and any www.), otherwise the
browser blocks the call.
Errors
Errors come back as JSON — { "error": "…" } — with a matching HTTP status.
| Status | Meaning | What to do |
|---|---|---|
401 | The key is missing, wrong, or has been revoked. | Check the header name and the key value. If the key was revoked in Settings → Integrations → Public API, create a new one and swap it in. |
403 | The request’s origin isn’t on the key’s domain allowlist. | Add the exact origin (scheme + host, e.g. https://www.example.com) to the key, or clear the allowlist. Server-side calls aren’t affected. |
429 | Too many requests for this key. | Back off and retry after the number of seconds in the Retry-After response header. Add caching — see below. |
503 | The Public API is switched off for the brand, or the subscription has lapsed. | Nothing to fix in code. Check Settings → Integrations → Public API is on and the Korea Import add-on is active. Show your visitors a fallback and retry later. |
Treat everything except 200 as “show the page without the feed” rather than
letting an error break the page.
Domain allowlist
Each key can carry a list of allowed origins. When the list is set, the API only returns CORS headers for those origins — a browser on any other site refuses the response. When the list is empty, browsers on any site are allowed.
Be clear about what that buys you: the allowlist stops other websites from
using your key in a browser. It is not a secret-keeper. Anyone who reads
your page source can copy the key and call the API from a server or from curl,
where CORS doesn’t apply.
So:
- Use the allowlist to stop casual reuse of your key on other sites.
- Rotate the key if you think it’s been taken. It’s a two-minute job.
- Don’t put anything behind this key that you wouldn’t publish on your own website — which is exactly why the response contains no cost data.
Rate limits
The default is 120 requests per minute per key. That’s generous for a website and tight for a scraper.
When you exceed it you get 429 with a Retry-After header in seconds. Respect
it — retrying immediately just extends the block.
The practical advice:
- Cache on your side. The feed changes at most every few minutes, so a 5- to 10-minute cache is invisible to visitors and removes almost all your traffic.
- Fetch once per page, not once per card. Pull a page of cars and render from that array.
- Don’t proxy every visitor. If your site is server-rendered, fetch the feed on a schedule into your own cache rather than on each page view.
What is not in the response, and why
The feed carries the retail car — what a customer sees. It never includes:
- the Korean purchase price, or anything in won
- your landed cost, duty, freight or fees
- the pricing rule that produced the price
- any link, ID or reference back to the Korean source listing
- the Korean seller’s name, type or city
That’s deliberate, and it’s on your side. Your website is public, so anything in
this feed is public: a competitor with curl would otherwise be able to read
your buying price, work out your margin, and find your supply chain. What they
get instead is your shop window — the same thing your customers get.
If you need cost figures for internal use, they stay in Scriptiflow under Korea Import, behind your team’s login.
Troubleshooting
Everything returns 401.
The header is X-Scriptiflow-Key and the value is the full key including the
sf_pk_live_ prefix — not the shortened version shown in the panel. If you only
have the shortened one, the full key is gone; create a new key.
It works from my terminal but not from the browser.
That’s CORS. Add your site’s exact origin to the key’s domain allowlist, or clear
the allowlist. A trailing slash, a missing www., or http:// instead of
https:// all count as a different origin.
Cars disappeared from my site. A car leaves the feed when it’s sold or when you unpublish it in Korea Import. Check the car is still in your selection and that the brand’s Public API setting still covers it.
Everything returns 503. The Public API switch is off, or the Korea Import add-on isn’t active on the brand. See Settings → Integrations → Public API, or contact support@scriptiflow.com.