文档
LotShow API
Send a lot photo, get a studio photo back. The API runs the same showroom renderer as the web Studio, on the same credit balance, so a photo uploaded in the Studio and a photo posted from your DMS cost the same.
https://api.lotshow.com/v1Everything is a job. You post one photo and a backdrop, we return a job id straight away, and about a minute later the finished image is ready. Register a webhook and we call you; or poll the job. The finished photo also lands in the account's Inventory, grouped under the vehicle you named.
Authentication
Create a key in Settings, under API keys. Keys start with ls_live_ and are shown once, so store it somewhere safe. An account can hold up to 10 live keys, and a key stops working if the account drops to the free plan.
Send the key as a bearer token on every request.
Authorization: Bearer ls_live_••••••••••••••••••••••••••••3f9a
Keys carry the full power of the account. Keep them on your server, never in a browser or an app bundle, and revoke any key you think has leaked from the same Settings page.
请从您的服务器调用 API。浏览器无法调用:API 不发送任何 CORS 响应头,网页无法调用它,而放在网页里的密钥任何人都能复制。
Quickstart
Check the key first. This returns your balance and plan.
curl https://api.lotshow.com/v1/credits \ -H "Authorization: Bearer $LOTSHOW_KEY"
{
"balance": 320,
"plan": "elite",
"monthly_credits": 1000,
"renews": "2026-11-13"
}Then send a photo. One credit is held for the job.
curl https://api.lotshow.com/v1/jobs \ -H "Authorization: Bearer $LOTSHOW_KEY" \ -F image=@stock-4471.jpg \ -F backdrop=grey-concrete \ -F vehicle="Stock 4471" \ -F webhook=https://yourdms.com/hooks/lotshow
const form = new FormData();
form.append("image", new Blob([await fs.readFile("stock-4471.jpg")]), "stock-4471.jpg");
form.append("backdrop", "grey-concrete");
form.append("vehicle", "Stock 4471");
form.append("webhook", "https://yourdms.com/hooks/lotshow");
const res = await fetch("https://api.lotshow.com/v1/jobs", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.LOTSHOW_KEY}` },
body: form,
});
console.log(await res.json());import os, requests
res = requests.post(
"https://api.lotshow.com/v1/jobs",
headers={"Authorization": f"Bearer {os.environ['LOTSHOW_KEY']}"},
files={"image": open("stock-4471.jpg", "rb")},
data={
"backdrop": "grey-concrete",
"vehicle": "Stock 4471",
"webhook": "https://yourdms.com/hooks/lotshow",
},
)
print(res.json()){
"id": "Hn4kR2pXq8VbT1sLw0Za",
"status": "queued",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"credits_left": 319
}About a minute later your webhook receives the finished photo. Or fetch the job yourself:
curl https://api.lotshow.com/v1/jobs/Hn4kR2pXq8VbT1sLw0Za \ -H "Authorization: Bearer $LOTSHOW_KEY"
When status is completed, output_url is a permanent link to the image. Use it on your listing page.
Photo jobs
A job turns one photo into one studio photo. It moves through four states.
| Status | Meaning |
|---|---|
queued | Accepted and waiting for a renderer. One credit is held. |
processing | The car is being cut out and placed in the backdrop. |
completed | Done. output_url is set. |
failed | The photo could not be finished. The credit goes back and error says why. |
Sending the photo
Post it as a multipart image file, or send JSON with an image_url we download for you. JPEG, PNG and WebP are accepted, up to 15 MB. We straighten and shrink the photo to at most 2048 px before it is rendered, the same as the web Studio does. A photo that isn't the whole car seen from outside fails, and its credit comes back.
Grouping by vehicle
Every job belongs to a vehicle in the account's Inventory. Pass vehicle_id to add to an existing one, or a vin or stock number and we find that vehicle, creating it on first use. A vehicle name works the same way. With none of these, photos go into a vehicle called API. Sending several angles of one car with the same VIN keeps them together.
Vehicles
A vehicle is a car in the account's Inventory with its photos, whether it was made through the API or in the web Studio. Key it by VIN or stock number, the same fields the Studio and the feed export use, and your system never has to store our ids.
| Status | Meaning |
|---|---|
empty | No photos yet. |
processing | At least one photo is queued or processing. |
completed | Every photo is finished. |
partial | Every photo has finished, and at least one failed. |
The shot checklist
shots says which of the eight exterior angles the vehicle's photos cover, read from each photo once it is processed: front, front34_left, side_left, rear34_left, rear, rear34_right, side_right and front34_right. A failed photo covers nothing.
Walkaround order
Photos are listed and sent to your feed round the car: front, front right three quarter, right side, rear right three quarter, rear, rear left three quarter, left side, front left three quarter, then any photo without an angle in the order it was sent.
Feed export
With Feed export set up in Settings, POST /v1/vehicles/{id}/feed uploads the finished photos to your listing feed provider over FTP or SFTP, named by VIN or stock number, the same as the Studio's send button.
DMS sync
Your DMS or inventory tool can run the whole flow on its own, keyed by VIN.
- Create the vehicle.
POST /v1/vehicleswith itsvinandstock. This step is optional: the first photo sent with a new VIN creates it too. - Post the photos. One
POST /v1/jobsper photo with the samevin. Photos sent in parallel land in the one vehicle. - Receive
vehicle.completed. Once every photo has finished, the event carries the vehicle and its finished photos in walkaround order. - Use the photos. Download each
output_urlinto your listing, fetchGET /v1/vehicles/{id}/photos, or callPOST /v1/vehicles/{id}/feedto push them to your feed provider.
# 1. The vehicle, keyed by its VIN
curl https://api.lotshow.com/v1/vehicles \
-H "Authorization: Bearer $LOTSHOW_KEY" \
-H "Idempotency-Key: create-1HGCV1F34PA004471" \
-H "Content-Type: application/json" \
-d '{"vin": "1HGCV1F34PA004471", "stock": "4471"}'
# 2. Each photo, sent with the same VIN
for f in lot/4471/*.jpg; do
curl https://api.lotshow.com/v1/jobs \
-H "Authorization: Bearer $LOTSHOW_KEY" \
-H "Idempotency-Key: $(basename "$f")-1HGCV1F34PA004471" \
-F image=@"$f" \
-F vin=1HGCV1F34PA004471
doneconst api = (path, init = {}) => fetch(`https://api.lotshow.com/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.LOTSHOW_KEY}`, ...init.headers },
});
// 1. The vehicle, keyed by its VIN
await api("/vehicles", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": "create-1HGCV1F34PA004471" },
body: JSON.stringify({ vin: "1HGCV1F34PA004471", stock: "4471" }),
});
// 2. Each photo, sent with the same VIN
for (const file of photos) {
const form = new FormData();
form.append("image", new Blob([await fs.readFile(file)]), path.basename(file));
form.append("vin", "1HGCV1F34PA004471");
await api("/jobs", { method: "POST", headers: { "Idempotency-Key": `${path.basename(file)}-1HGCV1F34PA004471` }, body: form });
}import os, requests
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['LOTSHOW_KEY']}"
# 1. The vehicle, keyed by its VIN
api.post(
"https://api.lotshow.com/v1/vehicles",
headers={"Idempotency-Key": "create-1HGCV1F34PA004471"},
json={"vin": "1HGCV1F34PA004471", "stock": "4471"},
)
# 2. Each photo, sent with the same VIN
for path in photos:
api.post(
"https://api.lotshow.com/v1/jobs",
headers={"Idempotency-Key": f"{os.path.basename(path)}-1HGCV1F34PA004471"},
files={"image": open(path, "rb")},
data={"vin": "1HGCV1F34PA004471"},
)Then, when vehicle.completed arrives, push the photos to the feed:
curl -X POST https://api.lotshow.com/v1/vehicles/pQ7mT2vK9cLx4RbN1sHd/feed \ -H "Authorization: Bearer $LOTSHOW_KEY" \ -H "Idempotency-Key: feed-1HGCV1F34PA004471-1"
{
"ok": true,
"sent": 8
}To find a vehicle later from your own records, filter by VIN or stock number: GET /v1/vehicles?vin=1HGCV1F34PA004471.
Backdrops
六个展厅共有 13 种背景:白色大厅有 8 种墙面和地面组合,其余五个展厅各有一种。车漆、饰条和车标绝不会被重绘;车辆会按展厅重新打光,周围的场景随之改变。请把 slug 传给 backdrop。不传时使用 grey-concrete。
| Slug | Name |
|---|---|
grey-concrete | Grey concrete |
white-hall | White hall |
white-tiles | White tiles |
micro-cement | Micro-cement |
stone | Stone |
plain-stone | Plain stone |
dealership | Dealership |
marble-hall | Marble hall |
photo-booth | Photo booth |
white-studio | White studio |
white-hall-plain | White hall, plain walls |
white-tiles-plain | White tiles, plain walls |
micro-cement-plain | Micro-cement, plain walls |
Fetch GET /v1/backdrops for a preview image of each.
Webhooks
Webhooks save you polling. There are two ways to receive them, and you can use both.
- Per job. Pass a
webhookURL when you create the job. Only that job's events go there. - Per account. Register up to 5 endpoints with
POST /v1/webhooksand choose the events each one wants.
Events
| Event | Sent when |
|---|---|
job.completed | A studio photo is ready. |
job.failed | A photo could not be finished. The credit is refunded. |
vehicle.completed | Every photo of a vehicle has finished and at least one is done. Sent once, and again only after more photos are added. Endpoints only. |
credits.low | Your balance drops under 20 credits. |
Payloads
Each delivery is a JSON POST with the event name, a timestamp, and the fields below.
{
"event": "job.completed",
"created_at": "2026-10-14T09:42:14.000Z",
"id": "Hn4kR2pXq8VbT1sLw0Za",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…",
"seconds": 62,
"credits_left": 319
}{
"event": "job.failed",
"created_at": "2026-10-14T09:42:02.000Z",
"id": "Hn4kR2pXq8VbT1sLw0Za",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"error": "Couldn't finish this photo.",
"credits_refunded": 1,
"credits_left": 320
}{
"event": "vehicle.completed",
"created_at": "2026-10-14T09:43:10.000Z",
"vehicle": {
"id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "1HGCV1F34PA004471",
"vin": "1HGCV1F34PA004471",
"stock": "4471",
"backdrop": "grey-concrete",
"photos_total": 8,
"photos_done": 8,
"photos_failed": 0,
"status": "completed",
"shots": {
"front": true,
"front34_left": true,
"side_left": true,
"rear34_left": true,
"rear": true,
"rear34_right": true,
"side_right": true,
"front34_right": true
},
"feed": null,
"created_at": "2026-10-14T09:40:02.000Z",
"updated_at": "2026-10-14T09:43:10.000Z"
},
"photos": [
{
"id": "Hn4kR2pXq8VbT1sLw0Za",
"angle": "front",
"photo_quality": "good",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…"
},
{
"id": "Tq9cW3nLx7HbR2kPv5Ms",
"angle": "front34_right",
"photo_quality": "good",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…"
}
]
}{
"event": "credits.low",
"created_at": "2026-10-14T09:50:11.000Z",
"balance": 18,
"threshold": 20
}Verifying a delivery
Every delivery is signed with your account's signing secret (it starts with whsec_ and is in Settings, next to your webhook endpoints). We use the Standard Webhooks scheme, so any library for it works. Three headers come with each request:
| Header | Value |
|---|---|
webhook-id | Unique id of the delivery. |
webhook-timestamp | Unix time in seconds. Reject anything more than five minutes old. |
webhook-signature | v1, followed by a base64 HMAC-SHA256 of id.timestamp.body. |
Compute the HMAC over the raw request body, not a re-serialised copy.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, rawBody, headers) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
return headers["webhook-signature"].split(" ").some((entry) => {
const [version, sig] = entry.split(",");
const got = Buffer.from(sig ?? "", "base64");
return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
});
}import base64, hashlib, hmac, time
def verify(secret: str, raw_body: str, headers: dict) -> bool:
msg_id = headers["webhook-id"]
ts = headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{ts}.{raw_body}".encode()
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for entry in headers["webhook-signature"].split(" "):
version, _, sig = entry.partition(",")
if version == "v1" and hmac.compare_digest(sig, expected):
return True
return FalseDelivery and retries
Answer with any 2xx within 10 seconds to acknowledge. Anything else counts as a failure and we retry after 1, 5, 15, 60, 180, 360 and 720 minutes, then stop. Recent deliveries and their status codes are listed in Settings.
Credits
The API and the web Studio spend one shared balance. A finished photo costs one credit, whoever asked for it. Credits come from your plan each month and from top-up packs, and both are spendable by the API. See pricing for plans and packs.
A credit is held when a job is accepted. If the job fails, it is returned automatically and credits_charged on the job reads 0. When a request would overdraw the balance it is refused with 402 insufficient_credits.
Jobs
The job object:
| Field | Type | Description |
|---|---|---|
id | string | Job id. |
status | string | queued, processing, completed or failed. |
backdrop | string | Backdrop slug. |
vehicle_id | string | The Inventory vehicle the photo belongs to. |
name | string | The label you sent, or the file name. |
output_url | string or null | Permanent link to the finished photo. Set once completed. |
photo_quality | string or null | good, soft or poor: how sharp and clean the car in the photo was. |
angle | string or null | Which of the eight exterior shots the photo is, such as front34_left. |
error | string or null | Why a job failed. |
credits_charged | integer | 1, or 0 when a failed job was refunded. |
seconds | integer or null | Time from request to finished photo. |
created_at | string | ISO 8601 time. |
completed_at | string or null | ISO 8601 time. |
创建任务
/v1/jobsTurns one photo into a studio photo. Send a multipart form with an image file, or a JSON body with an image_url. Returns right away; the photo follows by webhook or by polling.
发送 Idempotency-Key 请求头,即可在超时后安全重试。24 小时内,使用相同密钥和相同请求体的重试会拿回第一次的响应,并带有 Idempotent-Replayed: true,而不会再创建一个任务、多花一个点数。
请求头
- Idempotency-Keystring
- 任意唯一字符串,最多 255 个字符,例如 UUID。可选。
Parameters
- imagefilerequired
- The photo, as a multipart file. JPEG, PNG or WebP, up to 15 MB.
- image_urlstring
- A public http(s) URL we download instead of an upload. Use it in a JSON body.
- backdropstring
- A backdrop slug. Defaults to
grey-concrete. - vehicle_idstring
- An existing vehicle to add the photo to. Wins over everything below.
- vinstring
- The vehicle's VIN, up to 17 letters and digits without I, O or Q. Finds the vehicle, or creates it named after the VIN. Wins over stock and vehicle.
- stockstring
- The vehicle's stock number, up to 32 characters. Finds the vehicle, or creates it named after the number. Wins over vehicle.
- vehiclestring
- A vehicle name, up to 80 characters. Created on first use. Defaults to
API. - namestring
- A label for the photo, up to 120 characters. Defaults to the file name.
- webhookstring
- A public https URL that receives this job's events.
{
"id": "Hn4kR2pXq8VbT1sLw0Za",
"status": "queued",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"credits_left": 319
}获取任务
/v1/jobs/{id}Returns one job. A job id from another account answers 404 job_not_found.
{
"id": "Hn4kR2pXq8VbT1sLw0Za",
"status": "completed",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…",
"photo_quality": "good",
"angle": "front34_left",
"error": null,
"credits_charged": 1,
"seconds": 62,
"created_at": "2026-10-14T09:41:12.000Z",
"completed_at": "2026-10-14T09:42:14.000Z"
}列出任务
/v1/jobsJobs created through the API, newest first. Page with starting_after while has_more is true.
Query parameters
- limitinteger
- Between 1 and 100. Defaults to 20.
- starting_afterstring
- A job id. Returns the jobs after it.
{
"data": [
{
"id": "Hn4kR2pXq8VbT1sLw0Za",
"status": "completed",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…",
"photo_quality": "good",
"angle": "front34_left",
"error": null,
"credits_charged": 1,
"seconds": 62,
"created_at": "2026-10-14T09:41:12.000Z",
"completed_at": "2026-10-14T09:42:14.000Z"
}
],
"has_more": true
}Vehicles
The vehicle object:
| Field | Type | Description |
|---|---|---|
id | string | Vehicle id. Jobs carry it as vehicle_id. |
name | string | The name shown in Inventory. |
vin | string or null | VIN, upper case. |
stock | string or null | Stock number. |
backdrop | string or null | The vehicle's backdrop slug. |
photos_total | integer | Photos on the vehicle. |
photos_done | integer | Photos completed. |
photos_failed | integer | Photos that failed. |
status | string | empty, processing, completed or partial. |
shots | object | Each of the eight exterior angles, true when a photo covers it. |
feed | object or null | The last feed send: at, ok and count. |
created_at | string | ISO 8601 time. |
updated_at | string | ISO 8601 time. |
Create a vehicle
/v1/vehiclesCreates an empty vehicle. Send a JSON body. A VIN or stock number another vehicle of the account already has answers 409 vehicle_exists with that vehicle's vehicle_id.
请求头
- Idempotency-Keystring
- 任意唯一字符串,最多 255 个字符,例如 UUID。可选。
Parameters
- vinstring
- Up to 17 letters and digits without I, O or Q. Stored in upper case.
- stockstring
- Up to 32 characters.
- namestring
- Up to 80 characters. Defaults to the VIN, else the stock number.
{
"id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "1HGCV1F34PA004471",
"vin": "1HGCV1F34PA004471",
"stock": "4471",
"backdrop": "grey-concrete",
"photos_total": 0,
"photos_done": 0,
"photos_failed": 0,
"status": "empty",
"shots": {
"front": false,
"front34_left": false,
"side_left": false,
"rear34_left": false,
"rear": false,
"rear34_right": false,
"side_right": false,
"front34_right": false
},
"feed": null,
"created_at": "2026-10-14T09:40:02.000Z",
"updated_at": "2026-10-14T09:40:02.000Z"
}Retrieve a vehicle
/v1/vehicles/{id}Returns one vehicle. A vehicle id from another account answers 404 vehicle_not_found.
{
"id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "1HGCV1F34PA004471",
"vin": "1HGCV1F34PA004471",
"stock": "4471",
"backdrop": "grey-concrete",
"photos_total": 8,
"photos_done": 8,
"photos_failed": 0,
"status": "completed",
"shots": {
"front": true,
"front34_left": true,
"side_left": true,
"rear34_left": true,
"rear": true,
"rear34_right": true,
"side_right": true,
"front34_right": true
},
"feed": {
"at": "2026-10-14T09:45:30.000Z",
"ok": true,
"count": 8
},
"created_at": "2026-10-14T09:40:02.000Z",
"updated_at": "2026-10-14T09:43:10.000Z"
}List vehicles
/v1/vehiclesEvery vehicle on the account, newest first, including those made in the web Studio. Page with starting_after while has_more is true.
Query parameters
- vinstring
- Only the vehicle with this VIN.
- stockstring
- Only the vehicle with this stock number.
- limitinteger
- Between 1 and 100. Defaults to 20.
- starting_afterstring
- A vehicle id. Returns the vehicles after it.
{
"data": [
{
"id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "1HGCV1F34PA004471",
"vin": "1HGCV1F34PA004471",
"stock": "4471",
"backdrop": "grey-concrete",
"photos_total": 8,
"photos_done": 8,
"photos_failed": 0,
"status": "completed",
"shots": {
"front": true,
"front34_left": true,
"side_left": true,
"rear34_left": true,
"rear": true,
"rear34_right": true,
"side_right": true,
"front34_right": true
},
"feed": {
"at": "2026-10-14T09:45:30.000Z",
"ok": true,
"count": 8
},
"created_at": "2026-10-14T09:40:02.000Z",
"updated_at": "2026-10-14T09:43:10.000Z"
}
],
"has_more": false
}Update a vehicle
/v1/vehicles/{id}Changes the fields you send and leaves the rest. Send null to clear vin or stock. Vehicles are never deleted through the API.
Parameters
- namestring
- Up to 80 characters.
- vinstring or null
- Up to 17 letters and digits without I, O or Q.
- stockstring or null
- Up to 32 characters.
{
"id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "1HGCV1F34PA004471",
"vin": "1HGCV1F34PA004471",
"stock": "4471",
"backdrop": "grey-concrete",
"photos_total": 8,
"photos_done": 8,
"photos_failed": 0,
"status": "completed",
"shots": {
"front": true,
"front34_left": true,
"side_left": true,
"rear34_left": true,
"rear": true,
"rear34_right": true,
"side_right": true,
"front34_right": true
},
"feed": {
"at": "2026-10-14T09:45:30.000Z",
"ok": true,
"count": 8
},
"created_at": "2026-10-14T09:40:02.000Z",
"updated_at": "2026-10-14T09:43:10.000Z"
}List a vehicle's photos
/v1/vehicles/{id}/photosEvery photo of the vehicle as a job object, in walkaround order.
{
"data": [
{
"id": "Hn4kR2pXq8VbT1sLw0Za",
"status": "completed",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…",
"photo_quality": "good",
"angle": "front",
"error": null,
"credits_charged": 1,
"seconds": 62,
"created_at": "2026-10-14T09:41:12.000Z",
"completed_at": "2026-10-14T09:42:14.000Z"
},
{
"id": "Tq9cW3nLx7HbR2kPv5Ms",
"status": "completed",
"backdrop": "grey-concrete",
"vehicle_id": "pQ7mT2vK9cLx4RbN1sHd",
"name": "stock-4471",
"output_url": "https://firebasestorage.googleapis.com/v0/b/…/default.jpg?alt=media&token=…",
"photo_quality": "good",
"angle": "front34_right",
"error": null,
"credits_charged": 1,
"seconds": 62,
"created_at": "2026-10-14T09:41:12.000Z",
"completed_at": "2026-10-14T09:42:14.000Z"
}
]
}Send to your feed
/v1/vehicles/{id}/feedUploads the vehicle's finished photos to the feed set up in Settings, in walkaround order, named by VIN or stock number. Needs Feed export set up and the VIN or stock number its naming uses. An account can send 30 times an hour. If the server refuses part way, the answer is 502 feed_failed with a reason and how many photos were sent.
请求头
- Idempotency-Keystring
- 任意唯一字符串,最多 255 个字符,例如 UUID。可选。
{
"ok": true,
"sent": 8
}Backdrops
列出背景
/v1/backdrops全部 13 种背景,每种都带预览图。
{
"data": [
{
"slug": "grey-concrete",
"name": "Grey concrete",
"preview_url": "https://lotshow.com/assets/looks/grey.jpg"
},
{
"slug": "white-hall",
"name": "White hall",
"preview_url": "https://lotshow.com/assets/looks/original.jpg"
},
{
"slug": "white-tiles",
"name": "White tiles",
"preview_url": "https://lotshow.com/assets/looks/tiles.jpg"
},
{
"slug": "micro-cement",
"name": "Micro-cement",
"preview_url": "https://lotshow.com/assets/looks/cement.jpg"
},
{
"slug": "stone",
"name": "Stone",
"preview_url": "https://lotshow.com/assets/looks/stone.jpg"
},
{
"slug": "plain-stone",
"name": "Plain stone",
"preview_url": "https://lotshow.com/assets/looks/stoneflat.jpg"
},
{
"slug": "dealership",
"name": "Dealership",
"preview_url": "https://lotshow.com/assets/looks/dealer.jpg"
},
{
"slug": "marble-hall",
"name": "Marble hall",
"preview_url": "https://lotshow.com/assets/looks/marble.jpg"
},
{
"slug": "photo-booth",
"name": "Photo booth",
"preview_url": "https://lotshow.com/assets/looks/booth.jpg"
},
{
"slug": "white-studio",
"name": "White studio",
"preview_url": "https://lotshow.com/assets/looks/studio.jpg"
},
{
"slug": "white-hall-plain",
"name": "White hall, plain walls",
"preview_url": "https://lotshow.com/assets/looks/originalflat.jpg"
},
{
"slug": "white-tiles-plain",
"name": "White tiles, plain walls",
"preview_url": "https://lotshow.com/assets/looks/tilesflat.jpg"
},
{
"slug": "micro-cement-plain",
"name": "Micro-cement, plain walls",
"preview_url": "https://lotshow.com/assets/looks/cementflat.jpg"
}
]
}获取背景
/v1/backdrops/{slug}One backdrop. An unknown slug answers 404 backdrop_not_found.
{
"slug": "micro-cement",
"name": "Micro-cement",
"preview_url": "https://lotshow.com/assets/looks/cement.jpg"
}Credits
获取余额
/v1/creditsThe shared balance across the web Studio and the API, with the plan it comes from. renews is a date, or null without a monthly plan.
{
"balance": 320,
"plan": "elite",
"monthly_credits": 1000,
"renews": "2026-11-13"
}每日用量
/v1/credits/usagePhotos spent per day, oldest first.
Query parameters
- daysinteger
- Between 1 and 90. Defaults to 14.
{
"days": [
{
"date": "2026-10-13",
"spent": 35
},
{
"date": "2026-10-14",
"spent": 29
}
]
}健康检查
健康检查
/v1/healthAPI 正常运行时返回 {"ok": true}。无需密钥。
{
"ok": true
}Webhooks
注册端点
/v1/webhooksUp to 5 endpoints per account. Send a JSON body.
Parameters
- urlstringrequired
- A public https URL.
- eventsstring[]
- Any of
job.completed,job.failed,vehicle.completed,credits.low. Defaults to the first two.
{
"id": "Wk2pR8sN4vQx1LmT6cZa",
"url": "https://yourdms.com/hooks/lotshow",
"events": [
"job.completed",
"job.failed"
],
"created_at": "2026-10-14T09:30:05.000Z"
}列出端点
/v1/webhooksEvery endpoint registered on the account.
{
"data": [
{
"id": "Wk2pR8sN4vQx1LmT6cZa",
"url": "https://yourdms.com/hooks/lotshow",
"events": [
"job.completed",
"job.failed"
],
"created_at": "2026-10-14T09:30:05.000Z"
}
]
}发送测试事件
/v1/webhooks/{id}/testPosts a sample event named test to the endpoint and reports how it answered.
{
"delivered": true,
"status_code": 200,
"ms": 142
}删除端点
/v1/webhooks/{id}Stops sending events to it.
{
"id": "Wk2pR8sN4vQx1LmT6cZa",
"deleted": true
}Errors
Failures use a normal HTTP status and a JSON body with a stable code you can branch on.
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits. Top up on https://lotshow.com/settings?tab=billing"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | 请求格式有误:ID、参数、请求体或请求头无效。 |
| 400 | image_required | 既没有发送 image 文件,也没有发送 image_url。 |
| 400 | unknown_backdrop | backdrop 不是 13 个 slug 之一。 |
| 400 | bad_webhook | webhook 必须是公开的 https URL。 |
| 400 | image_unreachable | 无法下载 image_url。 |
| 400 | image_unreadable | 该文件不是 JPEG、PNG 或 WebP 照片。 |
| 401 | missing_key | 缺少 Authorization: Bearer 请求头。 |
| 401 | invalid_key | 密钥格式有误、不存在或已被撤销。 |
| 402 | insufficient_credits | 余额不足以支付该任务。 |
| 403 | plan_required | 该账户不是专业版或精英版。 |
| 403 | email_not_verified | 请先验证账户邮箱。 |
| 403 | account_blocked | 该账户无法创建新任务。 |
| 403 | not_approved | 该账户尚未获批。 |
| 404 | not_found | 没有这个端点,或没有该 ID 的 webhook 端点。 |
| 404 | job_not_found | 此账户中没有该 ID 的任务。 |
| 404 | vehicle_not_found | 没有该 vehicle_id 的车辆。 |
| 404 | backdrop_not_found | 没有该 slug 的背景。 |
| 409 | limit_reached | 该账户已有 5 个 webhook 端点。 |
| 409 | idempotency_conflict | 该 Idempotency-Key 已用于不同的请求体,或其第一次请求仍在处理中。 |
| 409 | vehicle_exists | Another vehicle on the account already has that VIN or stock number. Its id is in vehicle_id. |
| 409 | no_photos | The vehicle has no finished photos to send. |
| 409 | feed_not_configured | Feed export isn't set up in Settings. |
| 409 | vin_required | The feed names files by VIN and the vehicle has no 17 character VIN. |
| 409 | stock_required | The feed names files by stock number and the vehicle has none. |
| 413 | image_too_large | 图片最大 15 MB。 |
| 429 | rate_limited | 请求或任务过多。请放慢速度后重试。 |
| 429 | feed_limit_reached | The account has sent to its feed 30 times this hour. |
| 502 | feed_failed | The feed server refused the connection or the upload. reason says why and sent how many photos landed. |
| 503 | busy | 渲染暂时暂停。请稍后重试。 |
| 500 | internal | 我们这边出了问题。请重试。 |
Limits
| Limit | Value |
|---|---|
| Requests per key | 120 per minute, bursts up to 120 |
| Live keys per account | 10 |
| Webhook endpoints per account | 5 |
| Image size | 15 MB |
| Webhook response time | 10 seconds |
| Page size for lists | 100 |
| Feed sends per account | 30 per hour, 60 photos each |
Rate limit headers
Each key has a bucket of 120 tokens that refills at 2 a second. A request spends one token and creating a job spends two, so an idle key can burst and a steady client gets 120 requests a minute. Every answer says where you stand:
| Header | Value |
|---|---|
RateLimit-Policy | 120;w=60;burst=120 |
RateLimit | limit=120, remaining=97, reset=12: tokens left, and seconds until the bucket is full again. |
X-RateLimit-Limit | 120 |
X-RateLimit-Remaining | Tokens left. |
X-RateLimit-Reset | Seconds until the bucket is full again. |
Retry-After | On a 429 only: seconds to wait before retrying. |
Need more throughput? Talk to us.