Overview
The Listwyse API turns your photographers' raw vehicle photos into finished listing photos. You send each photo with the car's stock number. We retouch it where it was taken, or place the car in a showroom, check it against your original, and send it back.
You send
A photo (a link or a file), the car's stock number, and which dealer it's for.
You get back
The finished photo as a JPEG, PNG or WebP, usually a few minutes later.
You pay
Credits for each finished photo. Anything that fails is free.
A retouch fixes what a quick shoot on the lot gets wrong, and the car stays exactly the same car: same paint, wheels and badges. Each dealer's settings decide how far it goes: whether dirt and water spots come off, the lot behind the car is tidied, or a crooked photo is straightened. It's built for companies that shoot for many dealers: each dealer keeps its own settings, and you name dealers, cars and photos by your own ids.
Quickstart
Four requests, from your key to a finished photo.
- 1
Check your key
Your Listwyse contact gives you an API key; it starts
sk_live_. Keep it on your server: anyone who has it can spend your credits. This call shows your account and your credits.curl "https://listwyse.com/api/v1/studio/account" \ -H "Authorization: Bearer $LISTWYSE_KEY" - 2
Add a dealer
Add each dealer you shoot for, with your own code for it. From then on you name the dealer by that code. (A dealership's own account is one dealer, so it skips this step and never names one.)
curl -X POST "https://listwyse.com/api/v1/studio/dealers" \ -H "Authorization: Bearer $LISTWYSE_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Riverton Acura", "code": "RIV-014" }' - 3
Send a photo
Send the photo's link with the car's stock number and the dealer's code. Give it your own
photoId: that's how you find it later and retry safely. The answer comes back at once, with the photoqueued.curl -X POST "https://listwyse.com/api/v1/studio/photos" \ -H "Authorization: Bearer $LISTWYSE_KEY" \ -H "Content-Type: application/json" \ -d '{ "photoId": "PMP-88231", "url": "https://cdn.example.com/shoots/88231.jpg", "stockNumber": "A1234", "dealerCode": "RIV-014" }' - 4
Get the finished photo
A few minutes later the photo is
completedandurlis the finished file. Ask for it by yourphotoId, or set a webhook and we'll tell you the moment it's ready.curl "https://listwyse.com/api/v1/studio/photos/PMP-88231" \ -H "Authorization: Bearer $LISTWYSE_KEY"
Keys and requests
- Every call goes to
https://listwyse.com/api/v1/studiofollowed by the path, such as/photos. - Every request carries your key in a header:
Authorization: Bearer sk_live_…. A key only sees its own account. Use it from your server, never from a browser or a phone app. - Requests and answers are JSON. Times are in UTC, written like
2026-10-24T15:05:40.000Z. - Every answer carries a
requestId(also theX-Request-Idheader). Quote it if you ask us about a call. - Your own ids work in paths:
GET /photos/PMP-88231finds the photo you sent asPMP-88231. Encode any characters a URL can't carry (encodeURIComponentin JavaScript).
How it works
Five ideas, and the API makes sense
What the API is made of, how a request turns into a finished photo, and what it costs.
The four things
Everything in the API is one of four things, each inside the one before.
Account
Your company
Holds your credits and your API key, with its webhook URL.
Dealer
Riverton Acura
A dealership you shoot for, with its own settings.
Vehicle
Stock A1234
One car at that dealer. Its first photo makes it.
Photo
photoId: PMP-88231
Photo
photoId: PMP-88232
Photo
photoId: PMP-88233
There's no call to make a vehicle. The first photo you send with a new stock number makes the car. Every later photo with the same stock number, at the same dealer, joins it. With no stock number, the VIN does the same job.
You can name everything by your own ids: dealers by your code (dealerCode), cars by stock number, photos by your photoId. Each also has our id (id) if you'd rather keep that.
Nothing waits
Making a photo takes a few minutes, so no request waits for it.
When you send a photo, the answer comes straight back (202 Accepted) with the photo queued. The work then happens on our side, and you hear it's done in one of two ways:
- We tell you. A webhook: the moment a photo finishes, we POST it to your server. Use this in production.
- You ask.
GET /photos/:iduntil its status iscompletedorfailed. Handy while you're building.
POST /photos
the photo's link, the stock number, the dealer
202 Accepted, at once
the photo, queued, and its credits held
- a few minutes
We download it, make it, and put it through quality assurance
POST to your webhook URL
photo.completed, with the finished photo in it
Reply 200 and save photo.url
the link keeps working
A photo's life
A photo has one status at a time and only moves forward. completed and failed are the two ends.
| status | What's happening | Its credits |
|---|---|---|
| queued | Received. We download it and work out what it shows. | Held |
| processing | Being made. | Held |
| quality_assurance | Being checked against your original. This can add a few minutes. | Held |
| completed | Done: url is the finished file. | Kept |
| failed | Couldn't be done, or didn't pass quality assurance: error says why. | Given back |
Every photo also has statusText: a short sentence for its status, ready to show in your own app (“Going through quality assurance”).
Quality assurance
Every photo goes through quality assurance before it reaches you.
We compare each finished photo with your original, looking for changes to the car itself: a badge or model name altered, a part in the wrong colour, the mileage changed, something on the car that isn't really there. When a photo needs it, we make it again and check it again, on our side.
- While this happens, the photo's status is
quality_assurance. It can add a few minutes to a photo, more when it's made again. - It passes: the photo is
completedand sent to you. - It doesn't pass: the photo is
failed, witherror.codeQUALITY_ASSURANCE_FAILED(“Did not pass quality assurance”). You aren't charged, and sending it again (the samephotoId) tries it again.
Quality assurance is always on and isn't a setting. Nothing waits for a person to approve it.
Credits
You pay for photos you get back finished, and nothing else.
Your account has a balance of credits and a price per photo; GET /account shows both. A photo's price is held from your balance the moment you send it. What happens to the hold:
| When | Credits |
|---|---|
| It's finished and sent back | Kept: that's the charge |
| It fails, or doesn't pass quality assurance | Given back |
| It's made again during quality assurance | Free |
| It isn't a photo of a car, so comes back as it was (edited: false) | Given back |
| You send the same photoId again | Free: you get that photo back. A failed one is tried again, priced like a new photo |
| You ask for a redo | Charged again: it's a new photo |
If your balance can't cover a call, nothing starts: you get 402 OUT_OF_CREDITS with what was needed. Every send answers with credits.held and credits.balance, so you always know where you stand. Credits count to one decimal place. Your Listwyse contact tops up your balance.
Guides
Building it in
Each part of an integration in turn: sending, hearing back, settings, and what to do when something goes wrong.
Sending photos
One request per photo, as your photographers upload them.
The photo itself
Send a public https link as url: a JPEG, PNG, WebP or GIF under 12MB. We download it within moments and keep our own copy, so the link only needs to work until the photo is processing.
Or upload the file itself: send multipart/form-data with a file field (under 4MB) and the other fields beside it as form fields, settings and output as JSON text.
curl -X POST "https://listwyse.com/api/v1/studio/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-F "file=@88231.jpg" \
-F "photoId=PMP-88231" \
-F "stockNumber=A1234" \
-F "dealerCode=RIV-014" \
-F "angle=frontAngle" \
-F 'output={"format":"png"}'Your id for the photo
photoId is your own id for the photo, any text up to 200 characters. Send it every time; it does two jobs.
- Retrying is safe. Send the same
photoIdtwice and you get the first photo back, not a second one, and you aren't charged again. So if a request times out, send it again. If that photo had failed, sending it again tries it again. - You can look it up by it:
GET /photos/PMP-88231.
A photoId belongs to one car. Sending it again with a different stock number is refused (409 PHOTO_ID_TAKEN).
The car and the dealer
stockNumber (or vin when there isn't one) says which car the photo is of. dealerCode (your code) or dealerId (ours) says which dealer. If you shoot for dealers, every photo must name one (400 DEALER_REQUIRED otherwise), so a car never lands at the wrong dealer.
What the photo shows
You don't need to tell us. We look at each photo and work out what it shows (the front, a side, the dashboard, a badge…), and make it to suit.
If your app already knows each shot for certain, you can send it as angle (the names are in the reference). Only send one you're sure of: we use it as given, so a wrong one makes a worse photo than leaving it out. A photo that isn't of a car (a window sticker, a banner) comes back as it was, free, with a note.
How it should come out
Photos follow their dealer's settings. Any send can change them for its own photos: background (keep, or a look), settings (the sliders) and output.format (jpeg, png or webp).
Limits
- A car holds up to 60 photos, any mix of them: ten badges or thirty cabin photos are fine.
- Uploaded files must be under 4MB; links, under 12MB.
- A car shot again? Remove its old photos (
DELETE /photos/:id) to make room.
Sending a whole car
POST /vehicles sends up to 60 photos of one car in one request.
Underneath, each photo is exactly a photo call: the same statuses, files and price. Four things are different:
- All or nothing on credits. The whole call is held at once. If your balance can't cover every new photo, none start.
- One signal at the end. Each photo still sends its own webhook, and
vehicle.completedarrives once every photo from the call is done, with all of them in it. - Links only. A vehicle call takes
urls, not file uploads. - Safe to repeat. Photos whose
photoIdyou've sent before come back as they are (their ids are listed inreused), so sending the call again after a timeout costs nothing. Any that had failed are tried again.
curl -X POST "https://listwyse.com/api/v1/studio/vehicles" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerCode": "RIV-014",
"photos": [
{
"photoId": "PMP-88231",
"url": "https://cdn.example.com/shoots/88231.jpg"
},
{
"photoId": "PMP-88232",
"url": "https://cdn.example.com/shoots/88232.jpg"
},
{
"photoId": "PMP-88233",
"url": "https://cdn.example.com/shoots/88233.jpg"
}
]
}'Which should I use?
Webhooks
A webhook is a request we send to your server when a photo finishes.
The webhook belongs to your API key: events for the photos sent with a key go to that key's URL.
Set it up
- Pick an https URL on your server and set it:
PUT /webhook. - Copy the
secretfrom the answer (GET /webhookshows it again any time). It signs every event, so you can tell they're from us. - Send yourself a test:
POST /webhook/test.GET /webhook/deliveriesthen shows what your server answered.
What we send
A POST with a JSON body, and these headers:
| Header | What it holds |
|---|---|
| X-Listwyse-Event | The event's name. |
| X-Listwyse-Delivery | The delivery's id (the same as id in the body). |
| X-Listwyse-Signature | The signature: t=…,v1=… (below). |
| Event | When | In data |
|---|---|---|
| photo.completed | A photo is done. | photo |
| photo.failed | A photo couldn't be done. | photo, with its error |
| vehicle.completed | Every photo from one vehicle call is done. | vehicle, with those photos, and batchId |
| webhook.test | You asked for a test. | a message |
{
"id": "e2b1c0de-0a8f-4c55-9b1e-6f3a2d7c9e10",
"event": "photo.completed",
"createdAt": "2026-10-24T15:05:40.000Z",
"data": {
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"stockNumber": "A1234",
"status": "completed",
"url": "https://…/p29031.jpg",
"format": "jpeg",
"edited": true,
"cost": 0.1,
"…": "the rest of the photo"
}
}
}Check it's from us
X-Listwyse-Signature looks like t=1729782340,v1=5f2b…. t is when it was sent, in seconds. v1 is an HMAC-SHA256, made with your secret, of t, a full stop and the raw body. Work it out yourself and compare. Check t is recent too, so an old event can't be replayed at you.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as it arrived, before any JSON parsing.
// header: the X-Listwyse-Signature header. secret: from GET /webhook.
export function isFromListwyse(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const wanted = Buffer.from(expected, "hex");
return given.length === wanted.length && timingSafeEqual(given, wanted);
}Answer fast, handle each event once
- Reply with any
2xxwithin 8 seconds, then do the work. Anything else (an error, a time-out, a redirect) counts as a failed delivery. - A failed delivery is tried again after 1, 5, 15 and 30 minutes, then 1, 2, 4, 8, 12, 24 and 24 hours apart: about three days in all.
GET /webhook/deliveriesshows every try, andPOST /webhook/deliveries/:id/resendsends one now. - A retry has the same
id. Keep the ids you've handled and skip repeats.
Or ask
Without a webhook, ask GET /photos/:id (or GET /vehicles/:id for a vehicle call) every 10 to 30 seconds until it's done.
async function waitForPhoto(photoId) {
for (;;) {
const res = await fetch(`https://listwyse.com/api/v1/studio/photos/${encodeURIComponent(photoId)}`, {
headers: { Authorization: `Bearer ${process.env.LISTWYSE_KEY}` },
});
const { photo } = await res.json();
if (photo.status === "completed" || photo.status === "failed") return photo;
await new Promise((resolve) => setTimeout(resolve, 15_000)); // 15 seconds
}
}Dealer settings
Settings decide how a dealer's photos come out, so each call doesn't have to say.
There are three layers, and the most specific one wins:
- Listwyse's defaults (the Default column below).
- Your account's own settings, at the dealer id
default:PATCH /dealers/default/settings. Every dealer follows them until it has its own. - A dealer's own settings. The first time you change one of a dealer's settings, it gets its own copy: what it followed, plus your change. From then on, changes to your account's settings don't reach it.
On top of those, one send can change background, settings or output for just its own photos.
| Setting | Choices | Default | What it does |
|---|---|---|---|
| background | "keep", or a look's id | keep | Keep the photo's own background, or put the car in a look. |
| outputFormat | jpeg · png · webp | jpeg | The file type photos come back as. |
| cleanup | 0 As shot · 1 Detailed | 0 As shot | How clean the car looks: dirt, dust and water spots. Dents and scratches always stay. |
| backgroundTidy | 0 As shot · 1 Tidied · 2 Staged | 0 As shot | How much of the lot behind the car is tidied, when the background is kept. |
| reflections | 0 Minimal · 1 Natural | 0 Minimal | How much of the surroundings shows in the paint and glass. |
| framing | 0 Reframed · 1 As shot | 1 As shot | Whether a crooked photo may be straightened and re-cropped, or keeps its framing. |
Send sliders as step numbers. GET /dealers/:id/settings shows what a dealer runs on now and lists every step and its name under options. The same settings show in the Listwyse app.Quality assurance isn't among them: it's always on.
curl -X PATCH "https://listwyse.com/api/v1/studio/dealers/RIV-014/settings" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"reflections": 1,
"outputFormat": "png"
}'Backgrounds and looks
A photo either keeps its background or goes into a look.
- "keep" (the default) retouches the photo where it was taken, cleaning the car and the lot behind it as far as the dealer's Clean-up and Background tidy say.
- A look places the car in a studio, a showroom or outdoors. You still get one finished photo for each photo you send, showing the same view of the car.
| Look id | Name | What it looks like |
|---|---|---|
| grey-studio-catalogue | Infinity | Seamless grey cyclorama. |
| panel-showroom-catalogue | Lumen | Gloss floor, light-panel ceiling. |
| showroom-set-v2 | Signature | Light-grey walls, a lit ceiling, a tiled floor. |
| outdoor-catalogue | Horizon | Empty field, open sky. |
Looks made for your account (a dealer's own showroom, say) appear in GET /templates, with ids starting custom:, once they're ready. A close-up a look has no shot for (a badge, say) is retouched where it was taken instead, and its note says so. No dealer branding (plates, floor mats, screens or wall signs) is added to photos made through the API.
Errors and retrying
Every error has the same shape: a stable code to act on, and a plain message.
{
"error": {
"code": "OUT_OF_CREDITS",
"message": "Not enough credits: this needs 0.3 and the account has 0.1."
},
"credits": { "balance": 0.1, "needed": 0.3, "pricePerPhoto": 0.1 },
"requestId": "req_7c1e…"
}| code | Status | What it means | What to do |
|---|---|---|---|
| INVALID_REQUEST | 400 | A field is missing or wrong. | message names the field: fix it and send again. |
| INVALID_JSON | 400 | The body isn't valid JSON. | Check how the body is built. |
| INVALID_BACKGROUND | 400 | No look with that id on your account. | Use "keep" or an id from GET /templates. |
| DEALER_REQUIRED | 400 | You shoot for dealers and the call didn't name one. | Send dealerCode or dealerId. |
| DUPLICATE_PHOTO_ID | 400 | One vehicle call has the same photoId twice. | Give each photo its own photoId. |
| UNSUPPORTED_IMAGE | 400 | An uploaded file isn't a JPEG, PNG, WebP or GIF. | Convert it, or send another file. |
| INVALID_WEBHOOK_URL | 400 | The webhook URL isn't https on the public internet. | Use a public https URL. |
| ONE_DEALER_ACCOUNT | 400 | Adding a dealer to a dealership's own account. | Ask us for an account that shoots for dealers. |
| UNAUTHORIZED | 401 | No key, or not a live one. | Check the Authorization header. |
| OUT_OF_CREDITS | 402 | Your balance can't cover it. Nothing started. | Top up, then send again. |
| NOT_FOUND, DEALER_NOT_FOUND | 404 | Not on your account. | Check the id or code. |
| PHOTO_IN_PROGRESS | 409 | A redo or removal of a photo that's still being made. | Wait until it's completed or failed. |
| PHOTO_ID_TAKEN | 409 | That photoId was already sent for another car. | Use a new photoId for this car. |
| PHOTO_NOT_STORED | 409 | A redo of a photo we never received (its download failed). | Send it again with POST /photos and the same photoId. |
| DEALER_CODE_TAKEN | 409 | Another of your dealers has that code. | Pick another code. |
| NO_WEBHOOK_URL | 409 | A test event with no webhook URL set. | Set one with PUT /webhook first. |
| DELIVERING | 409 | A resend of a delivery that's being sent right now. | Wait a moment and look again. |
| TOO_LARGE | 413 | An uploaded file is over 4MB. | Send it as a link instead. |
| INTERNAL_ERROR | 500 | Something went wrong on our side. | Send it again; quote requestId if it keeps happening. |
| UNAVAILABLE | 503 | We couldn't check your key just then. | Send it again in a moment. |
When a photo fails
A failed photo isn't an error answer. It's a photo with status failed and an error saying why, and you're never charged for one. To try again, send it again with the same photoId.
| error.code | Why |
|---|---|
| QUALITY_ASSURANCE_FAILED | Did not pass quality assurance, even after being made again on our side. |
| DOWNLOAD_FAILED | We couldn't download the link: it isn't a public https link, isn't an image, or is over 12MB. |
| ANGLE_UNKNOWN | We couldn't tell what the photo shows. Send it again. |
| VEHICLE_FULL | The car already has 60 photos. Remove old ones to make room. |
| LOOK_UNAVAILABLE | The look can't be used any more (it was deleted, or isn't ready). |
| START_FAILED | It couldn't be started; message says why. |
| GENERATION_FAILED | Every image service we use failed on this photo. |
| START_TIMEOUT | It never got going. Send it again. |
| VEHICLE_GONE | The car was deleted before the photo started. |
Retrying safely
- A network error, 500 or 503? Send exactly the same request again. With a
photoId, you get the first photo back if the first request did get through. - 409 PHOTO_IN_PROGRESS? Wait for the photo to finish, then try again.
- Any other 4xx? The request won't work as it is:
messagesays what to change.
Postman and AI tools
Postman, Insomnia and code generators
Every call is in one OpenAPI file: https://listwyse.com/api/v1/studio/openapi.json. In Postman, choose Import and paste that link, then set the collection's Authorization to Bearer Token with your key. Every call is then ready to try, with its fields and examples.
AI assistants
The same calls are tools for AI assistants that speak MCP (Claude Code, Cursor and others), at https://listwyse.com/api/mcp, with your key in the Authorization header. Ask in plain words (“send these photos for stock A1234 at Riverton”). Tools that use credits tell the assistant to check with you first.
claude mcp add --transport http listwyse https://listwyse.com/api/mcp \
--header "Authorization: Bearer sk_live_YOUR_KEY"{
"mcpServers": {
"listwyse": {
"url": "https://listwyse.com/api/mcp",
"headers": { "Authorization": "Bearer sk_live_YOUR_KEY" }
}
}
}Reference
Every call
Each call with its fields, what happens when you make it, the errors it can answer with, a request in curl, JavaScript or Python, and an example answer.
Objects
What the calls answer with, field by field. In a photo, a field with nothing in it yet is null rather than missing.
The photo
One photo you sent, and what became of it. Every photo call answers with one.
string
Our id for the photo.
string or null
Your id for it, as you sent it.
string
Our id for its car.
string or null
Its car's stock number and VIN.
string or null
Our id for its dealer.
queued, processing, quality_assurance, completed or failed
Where it is. Only completed and failed are final.
string
A plain sentence for the status, ready to show people.
string or null
The finished file, once completed.
jpeg, png or webp
The file type of url.
boolean or null
true: our finished photo. false: it isn't a photo of a car (a window sticker, say), so it came back as it was, free. null until completed.
string or null
What the photo shows (frontAngle, interior…): as you sent it, or as we worked it out.
given, detected or null
Whether you sent the angle or we worked it out.
string
"keep", or the look it was put in.
number
1, then 2 after a redo, and so on.
number
Credits this version cost: the price while held or kept, 0 once given back.
string or null
Why something out of the ordinary happened, such as your original being sent back.
object or null
On a failed photo: code and message say why.
string or null
The link you sent (null for an uploaded file).
string
When it was sent, last changed, and finished (UTC, ISO 8601).
Example
{
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
}The vehicle
One car at one dealer. It's made the first time a photo arrives with its stock number (or VIN).
string
Our id for the car.
string or null
How you named it. A VIN sent later is added to a car first sent by stock number.
string or null
Our id for its dealer.
empty, processing or completed
processing while any of its photos is still going.
number
How many photos it has.
object
How many of its photos are at each status.
list of photos
On GET /vehicles/:id (all of them) and POST /vehicles (that call's).
string
When its first photo arrived.
Example
{
"id": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "processing",
"photoCount": 12,
"counts": {
"queued": 0,
"processing": 3,
"quality_assurance": 1,
"completed": 8,
"failed": 0
},
"createdAt": "2026-10-24T15:02:11.000Z",
"photos": [
{
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
},
"…"
]
}The dealer
A dealership you shoot for.
string
Our id for the dealer: send it as dealerId.
string
Its name.
string or null
Your code for it: send it as dealerCode.
string
When it was added.
Example
{
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Riverton Acura",
"code": "RIV-014",
"createdAt": "2026-10-24T14:40:00.000Z"
}The webhook event
What we POST to your webhook URL. data holds the whole object, as GET would return it.
string
The delivery's id. A retry has the same id, so skip ids you've handled.
string
photo.completed, photo.failed, vehicle.completed or webhook.test.
string
When the event happened.
photo
On photo.completed and photo.failed.
vehicle, string
On vehicle.completed: the car, with every photo from that vehicle call.
Example
{
"id": "e2b1c0de-…",
"event": "photo.completed",
"createdAt": "2026-10-24T15:05:40.000Z",
"data": {
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
}
}
}The error
Every error has the same shape. Branch on error.code; error.message is plain English for logs and people.
string
A stable code, such as OUT_OF_CREDITS.
string
What went wrong and, where it helps, what to do.
string
Quote it if you ask us about the call.
object
On OUT_OF_CREDITS: balance, needed and pricePerPhoto.
Example
{
"error": {
"code": "OUT_OF_CREDITS",
"message": "Not enough credits: this needs 0.3 and the account has 0.1."
},
"credits": {
"balance": 0.1,
"needed": 0.3,
"pricePerPhoto": 0.1
},
"requestId": "req_7c1e…"
}Angle names
Optional. Leave angle out and we work out what each photo shows. Send one of these only when your app knows the shot for certain: we use it as given.
Photos
One photo in, one finished photo out. These are the calls most integrations use: send each photo as it's shot, then read it (or let the webhook tell you) when it's done.
/photosUses creditsSend a photo
Starts one photo and answers straight away with it queued. Send JSON with a link to the photo (url), or multipart/form-data with the photo as a file field (under 4MB) and the other fields beside it, settings and output as JSON text.
When to use it: You have one photo and want it back finished. This is the call most integrations make, once per photo, as photos are shot.
Body (JSON, or the same fields as a form with a file)
Your own id for the photo. Sending the same photoId again returns that photo instead of making (and charging for) it twice; if it had failed, it's tried again.
A public https link to the photo.
Optional. Leave it out and we work out what the photo shows. Send it only if your app already knows the shot (front, frontAngle, side, interior…); the docs list every name.
The dealer's stock number. Photos with the same stock number (and dealer) are one vehicle.
The 17-character VIN. Needed when there's no stockNumber.
Our id for the dealer (GET /dealers). Needed if you shoot for dealers; a dealership's own account leaves it out.
Or your own code for the dealer, set on POST /dealers.
"keep" retouches the photo where it was taken; a look id (GET /templates) puts the car in that look. Defaults to the dealer's setting.
Slider steps for these photos only, on top of the dealer's settings. Each is a step number: GET /dealers/:id/settings lists every step and its name.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.
Background tidy: how much of the lot behind the car is tidied, when the background is kept.
Reflections: how much of the surroundings shows in the paint and glass.
Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.
The file you get back.
jpeg (the default), png or webp.
What happens
- We check the request and hold the photo's price from your credits. The answer comes back at once: the photo, queued.
- We find the car by its stock number (or VIN) at that dealer, or make it if this is its first photo.
- In the background: we download the photo, work out what it shows, and make it.
- It goes through quality assurance: it's compared with your original, and made again automatically if it needs to be. This can add a few minutes.
- It ends completed (url is the finished file) or failed. A photo.completed or photo.failed webhook goes to your URL.
The answer: photo is the photo as it stands (queued). vehicle is its car, with how many photos it has. credits says what was held and what's left. A photoId you've sent before answers 200 with that photo and reused: true, and holds nothing; if that photo had failed, it's tried again instead (202, its version one higher).
Errors
curl -X POST "https://listwyse.com/api/v1/studio/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"photoId": "PMP-88231",
"url": "https://cdn.example.com/shoots/88231.jpg",
"stockNumber": "A1234",
"dealerCode": "RIV-014"
}'{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "queued",
"statusText": "Waiting to start",
"url": null,
"format": "jpeg",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:02:11.000Z",
"completedAt": null
},
"vehicle": {
"id": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "processing",
"photoCount": 12,
"counts": {
"queued": 0,
"processing": 3,
"quality_assurance": 1,
"completed": 8,
"failed": 0
},
"createdAt": "2026-10-24T15:02:11.000Z"
},
"credits": {
"pricePerPhoto": 0.1,
"held": 0.1,
"balance": 41.9
}
}/photos/:idCheck a photo
The photo as it stands now. Use our id or your own photoId in the path.
When to use it: You want a photo's latest status, or you don't use webhooks and are waiting for one to finish.
Path
Our photo id, or your photoId.
The answer: Once status is completed, url is the finished file. While it's still going, statusText is a sentence you can show. A failed photo carries error, with code QUALITY_ASSURANCE_FAILED when it didn't pass quality assurance.
Errors
curl "https://listwyse.com/api/v1/studio/photos/PMP-88231" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
}
}/photosList photos
Photos on your account, newest first, 50 at a time. Narrow them by vehicle, dealer or status.
When to use it: Catching up: say, every photo still processing, or a dealer's photos from today.
Query
Only this vehicle's photos.
Only this dealer's photos.
Only photos with this status.
How many (1–100, default 50).
Paging: the createdAt of the last item you have; you get older ones.
The answer: next is set when there are more: send it back as before for the page after.
curl "https://listwyse.com/api/v1/studio/photos?status=processing&limit=20" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"photos": [
{
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
}
],
"next": "2026-10-24T15:02:11.000Z"
}/photos/:id/redoUses creditsMake a photo again
Makes a completed or failed photo again as its next version. Change the background, the slider settings or the file type if you like; anything you leave out stays as it was.
When to use it: A finished photo isn't what the dealer wanted, or a failed one is worth another try.
Path
Our photo id, or your photoId.
Body
"keep" retouches the photo where it was taken; a look id (GET /templates) puts the car in that look. Defaults to the dealer's setting.
Slider steps for these photos only, on top of the dealer's settings. Each is a step number: GET /dealers/:id/settings lists every step and its name.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.
Background tidy: how much of the lot behind the car is tidied, when the background is kept.
Reflections: how much of the surroundings shows in the paint and glass.
Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.
The file you get back.
jpeg (the default), png or webp.
What happens
- The price is held again: a redo is a new photo.
- The photo goes back to queued with version one higher, and runs like a new photo, its checks asked afresh.
- The earlier version's file keeps working at its link.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/photos/PMP-88231/redo" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"cleanup": 1
},
"output": {
"format": "png"
}
}'{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "queued",
"statusText": "Waiting to start",
"url": null,
"format": "jpeg",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 2,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:02:11.000Z",
"completedAt": null
},
"credits": {
"pricePerPhoto": 0.1,
"held": 0.1,
"balance": 41.9
}
}/photos/:idRemove a photo
Takes a completed or failed photo off its car and forgets it. Its finished file keeps working at its link.
When to use it: A car is shot again and the old photos shouldn't count towards its 60.
Path
Our photo id, or your photoId.
Errors
curl -X DELETE "https://listwyse.com/api/v1/studio/photos/PMP-88231" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"deleted": true,
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11"
}Vehicles
A whole car in one request. Every photo in it becomes a photo like the ones above, with the same statuses, files and price, plus one vehicle.completed webhook when the last one is done.
/vehiclesUses creditsSend a vehicle's photos
Up to 60 photos of one car in one request, as links. Each becomes a photo exactly as if it had been sent to POST /photos.
When to use it: You collect a car's photos first and want one signal when the whole car is done.
Body
The dealer's stock number. Photos with the same stock number (and dealer) are one vehicle.
The 17-character VIN. Needed when there's no stockNumber.
Our id for the dealer (GET /dealers). Needed if you shoot for dealers; a dealership's own account leaves it out.
Or your own code for the dealer, set on POST /dealers.
The car's photos, up to 60.
Your own id for the photo. Sending the same photoId again returns that photo instead of making (and charging for) it twice; if it had failed, it's tried again.
A public https link to the photo.
Optional. Leave it out and we work out what the photo shows. Send it only if your app already knows the shot (front, frontAngle, side, interior…); the docs list every name.
"keep" retouches the photo where it was taken; a look id (GET /templates) puts the car in that look. Defaults to the dealer's setting.
Slider steps for these photos only, on top of the dealer's settings. Each is a step number: GET /dealers/:id/settings lists every step and its name.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.
Background tidy: how much of the lot behind the car is tidied, when the background is kept.
Reflections: how much of the surroundings shows in the paint and glass.
Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.
The file you get back.
jpeg (the default), png or webp.
What happens
- Every new photo's price is held together. If your balance can't cover them all, none start.
- The answer comes back at once: the vehicle, with this call's photos, queued.
- Each photo runs on its own and sends its own photo.completed or photo.failed webhook.
- When the last photo from this call is done, a vehicle.completed webhook carries all of them.
The answer: batchId names this call's photos; the vehicle.completed webhook carries the same one. reused lists photos whose photoId you'd sent before: they come back as they are, uncharged (a failed one is tried again instead).
Errors
curl -X POST "https://listwyse.com/api/v1/studio/vehicles" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerCode": "RIV-014",
"photos": [
{
"photoId": "PMP-88231",
"url": "https://cdn.example.com/shoots/88231.jpg"
},
{
"photoId": "PMP-88232",
"url": "https://cdn.example.com/shoots/88232.jpg"
},
{
"photoId": "PMP-88233",
"url": "https://cdn.example.com/shoots/88233.jpg"
}
]
}'{
"vehicle": {
"id": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "processing",
"photoCount": 3,
"counts": {
"queued": 0,
"processing": 3,
"quality_assurance": 1,
"completed": 8,
"failed": 0
},
"createdAt": "2026-10-24T15:02:11.000Z",
"photos": [
{
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "queued",
"statusText": "Waiting to start",
"url": null,
"format": "jpeg",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:02:11.000Z",
"completedAt": null
},
"…"
]
},
"batchId": "7d1f2c4e-…",
"credits": {
"pricePerPhoto": 0.1,
"held": 0.3,
"balance": 41.7
}
}/vehicles/:idCheck a vehicle
A car and every photo sent for it, oldest first.
When to use it: Waiting on a vehicle call without webhooks, or showing a car's progress.
Path
The vehicle's id (vehicleId on any of its photos).
The answer: status is processing while any photo is still going, then completed (empty if it has no photos). counts says how many photos are at each status.
Errors
curl "https://listwyse.com/api/v1/studio/vehicles/VEHICLE_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"vehicle": {
"id": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "processing",
"photoCount": 12,
"counts": {
"queued": 0,
"processing": 3,
"quality_assurance": 1,
"completed": 8,
"failed": 0
},
"createdAt": "2026-10-24T15:02:11.000Z",
"photos": [
{
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PMP-88231",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "completed",
"statusText": "Done",
"url": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/p29031.jpg",
"format": "jpeg",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"version": 1,
"cost": 0.1,
"note": null,
"error": null,
"inputUrl": "https://cdn.example.com/shoots/88231.jpg",
"createdAt": "2026-10-24T15:02:11.000Z",
"updatedAt": "2026-10-24T15:05:40.000Z",
"completedAt": "2026-10-24T15:05:40.000Z"
},
"…"
]
}
}/vehiclesList or find vehicles
Cars on your account, newest first, without their photos. With stockNumber (or vin) it finds that one car; name its dealer too if you shoot for dealers.
When to use it: Looking a car up by its stock number, or listing recent cars.
Query
Find one vehicle by its stock number (with dealerId or dealerCode).
Or by its VIN.
—
—
How many (1–100, default 50).
Paging: the createdAt of the last item you have; you get older ones.
Errors
curl "https://listwyse.com/api/v1/studio/vehicles?stockNumber=A1234&dealerCode=RIV-014" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"vehicles": [
{
"id": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"stockNumber": "A1234",
"vin": "1HGCV1F34PA012345",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"status": "processing",
"photoCount": 12,
"counts": {
"queued": 0,
"processing": 3,
"quality_assurance": 1,
"completed": 8,
"failed": 0
},
"createdAt": "2026-10-24T15:02:11.000Z"
}
],
"next": null
}Dealers and settings
The dealers you shoot for, each with its own settings. Settings decide how a dealer's photos come out when a call doesn't say otherwise; the id default is your account's own settings, which every dealer follows until it has its own.
/dealersList dealers
Every dealer on your account, by name. An account that is itself one dealer lists none.
curl "https://listwyse.com/api/v1/studio/dealers" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"dealers": [
{
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Riverton Acura",
"code": "RIV-014",
"createdAt": "2026-10-24T14:40:00.000Z"
}
],
"accountKind": "provider"
}/dealersAdd a dealer
Adds a dealer you shoot for. Give it your own code and you can name it by that code (dealerCode) on every call.
When to use it: You start shooting for a new dealership.
Body
The dealer's name.
Your own id for the dealer, to use as dealerCode.
The answer: A new dealer follows your account's settings (the id default) until you change one of its own.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/dealers" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Riverton Acura",
"code": "RIV-014"
}'{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Riverton Acura",
"code": "RIV-014",
"createdAt": "2026-10-24T14:40:00.000Z"
}
}/dealers/:idGet a dealer
Path
Our dealer id, or your code.
Errors
curl "https://listwyse.com/api/v1/studio/dealers/RIV-014" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Riverton Acura",
"code": "RIV-014",
"createdAt": "2026-10-24T14:40:00.000Z"
}
}/dealers/:idRename a dealer or change its code
Send only what changes. A code of null removes it.
Path
Our dealer id, or your code.
Body
—
Your own id for the dealer; null removes it.
Errors
curl -X PATCH "https://listwyse.com/api/v1/studio/dealers/RIV-014" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Riverton Acura Sacramento"
}'{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Riverton Acura",
"code": "RIV-014",
"createdAt": "2026-10-24T14:40:00.000Z"
}
}/dealers/:id/settingsGet a dealer's settings
How a dealer's photos come out when a call doesn't say otherwise, and every choice each setting has. Use the id default for your account's own settings.
Path
Our dealer id, your code, or default for your account's own settings.
The answer: inherited is true while the dealer follows your account's settings (or Listwyse's, when your account has none of its own). options lists every background, file type and slider step you can choose, with the steps' names.
Errors
curl "https://listwyse.com/api/v1/studio/dealers/RIV-014/settings" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"settings": {
"background": "keep",
"outputFormat": "jpeg",
"cleanup": 1,
"backgroundTidy": 1,
"reflections": 1,
"framing": 1
},
"inherited": false,
"options": {
"background": [
"keep",
"grey-studio-catalogue",
"panel-showroom-catalogue",
"…"
],
"outputFormat": [
"jpeg",
"png",
"webp"
],
"sliders": {
"cleanup": {
"name": "Clean-up",
"steps": [
{
"step": 0,
"label": "As shot",
"description": null
},
{
"step": 1,
"label": "Detailed",
"description": null
}
]
},
"backgroundTidy": {
"name": "Background tidy",
"steps": [
"…"
]
},
"reflections": {
"name": "Reflections",
"steps": [
"…"
]
},
"framing": {
"name": "Framing",
"steps": [
"…"
]
}
}
}
}/dealers/:id/settingsChange a dealer's settings
Send only the settings that change. From then on the dealer keeps settings of its own instead of following your account's.
When to use it: Setting a new dealer up, or changing how its photos come out from now on.
Path
Our dealer id, your code, or default for your account's own settings.
Body
The background photos get when a call doesn't say: "keep" or a look id.
The file type photos come back as when a call doesn't say.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number, as listed under options.
Background tidy: how much of the lot behind the car is tidied, when the background is kept. A step number.
Reflections: how much of the surroundings shows in the paint and glass. A step number.
Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing. A step number.
The answer: The settings as they now stand, in the same shape as GET. The same settings show in the Listwyse app.
Errors
curl -X PATCH "https://listwyse.com/api/v1/studio/dealers/RIV-014/settings" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"reflections": 1,
"outputFormat": "png"
}'{
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"settings": {
"background": "keep",
"outputFormat": "jpeg",
"cleanup": 1,
"backgroundTidy": 1,
"reflections": 1,
"framing": 1
},
"inherited": false,
"options": {
"background": [
"keep",
"grey-studio-catalogue",
"panel-showroom-catalogue",
"…"
],
"outputFormat": [
"jpeg",
"png",
"webp"
],
"sliders": {
"cleanup": {
"name": "Clean-up",
"steps": [
{
"step": 0,
"label": "As shot",
"description": null
},
{
"step": 1,
"label": "Detailed",
"description": null
}
]
},
"backgroundTidy": {
"name": "Background tidy",
"steps": [
"…"
]
},
"reflections": {
"name": "Reflections",
"steps": [
"…"
]
},
"framing": {
"name": "Framing",
"steps": [
"…"
]
}
}
}
}Webhooks
Where we tell you about finished photos. Set a URL, try it with a test event, and see every delivery and what your server answered.
/webhookYour webhook
The URL we send events to, the secret that signs them, and the events there are.
curl "https://listwyse.com/api/v1/studio/webhook" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"webhook": {
"url": "https://api.example.com/listwyse/events",
"secret": "9f86d081884c7d65…",
"events": [
"photo.completed",
"photo.failed",
"vehicle.completed",
"webhook.test"
]
}
}/webhookSet your webhook URL
An https URL on the public internet, or null to stop sending. The secret stays the same.
Body
Your https endpoint, or null to stop webhooks.
Errors
curl -X PUT "https://listwyse.com/api/v1/studio/webhook" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/listwyse/events"
}'{
"webhook": {
"url": "https://api.example.com/listwyse/events",
"secret": "9f86d081884c7d65…"
}
}/webhook/testSend a test event
Sends a webhook.test event to your URL now, signed like the real ones.
When to use it: Checking your endpoint receives and verifies events before any photo is sent.
The answer: The delivery. A moment later GET /webhook/deliveries shows what your server answered.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/webhook/test" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"delivery": {
"id": "e2b1…",
"event": "webhook.test",
"status": "queued"
}
}/webhook/deliveriesRecent deliveries
Every event we've sent (or are about to), newest first: what your server answered, how many tries, and when the next one is.
When to use it: Finding out why your server didn't get an event.
Query
How many (1–100, default 50).
Paging: the createdAt of the last item you have; you get older ones.
The answer: status is queued, delivering, delivered or failed. A failed one with nextAttemptAt will be tried again then; one without has run out of tries (resend it).
curl "https://listwyse.com/api/v1/studio/webhook/deliveries?limit=20" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"deliveries": [
{
"id": "e2b1…",
"event": "photo.completed",
"status": "failed",
"attempts": 2,
"responseStatus": 500,
"error": "Webhook endpoint returned HTTP 500.",
"nextAttemptAt": "2026-10-24T15:12:00.000Z",
"createdAt": "2026-10-24T15:05:40.000Z",
"deliveredAt": null,
"payload": {
"id": "e2b1…",
"event": "photo.completed",
"data": {
"photo": {
"id": "…"
}
}
}
}
],
"next": null
}/webhook/deliveries/:id/resendSend a delivery again
Sends one delivery again now, with the same id and body.
Path
The delivery's id.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/webhook/deliveries/DELIVERY_ID/resend" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"delivery": {
"id": "e2b1…",
"status": "queued"
}
}Account and looks
Your key, your credits, and the looks a photo can be put in.
/accountCheck your key and credits
Who the key belongs to, its credit balance and price per photo, and the limits.
When to use it: Your first call, to check the key works; and before a big send, to check the balance.
The answer: credits.balance is what's left to spend; credits.pricePerPhoto is what one finished photo costs. organization.kind is provider for an account that shoots for dealers (dealerships lists them) and dealership for one dealer's own account.
curl "https://listwyse.com/api/v1/studio/account" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"key": {
"id": "…",
"name": "Pro-MotionPix",
"prefix": "sk_live_1d8d34c6…",
"webhookUrl": "https://api.example.com/listwyse/events"
},
"organization": {
"id": "…",
"name": "Pro-MotionPix",
"kind": "provider"
},
"dealerships": [
{
"id": "d89df6f9-…",
"name": "Riverton Acura"
}
],
"credits": {
"balance": 41.9,
"pricePerPhoto": 0.1
},
"limits": {
"photosPerVehicle": 60,
"photosPerVehicleCall": 60,
"uploadBytes": 4194304
}
}/templatesList the looks
Every look a photo can be put in: Listwyse's own, and any your account has built. Use a look's id as background.
The answer: Skip looks with videoOnly: true; they only make films.
curl "https://listwyse.com/api/v1/studio/templates" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"templates": [
{
"id": "panel-showroom-catalogue",
"name": "Lumen",
"description": "Gloss floor, light-panel ceiling.",
"builtIn": true,
"videoOnly": false
},
{
"id": "custom:3b5f…",
"name": "Riverton showroom",
"builtIn": false,
"videoOnly": false
}
]
}Step-by-step calls
Before the photo and vehicle calls, the API worked on a vehicle as a session you filled, sorted and made yourself. Those calls still work, and they do some things the photo calls don't: importing a listing page, making new angles from a car's photos, films, dealer branding, and looks built from photos of a dealer's own building.
Dealership branding15 calls
Set up once per dealership and used on every new vehicle. The branding kit is the logo plus a display plate, screen splash and floor mat, each an AI design or a photo of one the dealer owns. A Build-your-own look is set in the dealer's own building, made from photos of it. Provider accounts send providerDealershipId (from GET /account) on each call; other accounts leave it out.
/brandingThe branding kit: the logo and, for each asset, its saved designs, which one is chosen, and whether one is being made.
Assets are displayPlate, monitor (the screen) and floorMat. New vehicles get the chosen designs, and one plate design serves the front and rear plate shots. Vehicles made earlier keep what they had.
curl "https://listwyse.com/api/v1/studio/branding" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"branding": {
"dealership": { "name": "Riverton Acura", "providerDealershipId": null },
"logoUrl": "https://…",
"assets": {
"displayPlate": {
"chosenUrl": "https://…",
"designs": [{ "url": "https://…", "uploaded": false }],
"generating": false,
"runId": null,
"error": null
},
"monitor": { "chosenUrl": null, "designs": [], "generating": true, "runId": "run_…", "error": null },
"floorMat": { "chosenUrl": "https://…", "designs": [{ "url": "https://…", "uploaded": true }], "generating": false, "runId": null, "error": null }
}
}
}/branding/logoSet the dealership logo: JSON { url } for a logo you host, or a multipart file field.
Transparent areas are flattened onto white. Designs made from an earlier logo stay in use until you make new ones.
curl -X POST "https://listwyse.com/api/v1/studio/branding/logo" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example/logo.png"
}'{ "branding": { "logoUrl": "https://…", "assets": { … } } }/branding/designsUses creditsPaid AI work: design a plate, screen and/or mat from the logo. One design each, chosen as soon as it lands.
kinds defaults to all three. Takes a minute or two: poll GET /branding until the asset's generating is false. 409 if every asked asset is already being designed; 400 without a logo.
curl -X POST "https://listwyse.com/api/v1/studio/branding/designs" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"kinds": [
"displayPlate",
"monitor",
"floorMat"
]
}'{ "branding": { "assets": { "displayPlate": { "generating": true, "runId": "run_…" } } } }/branding/photosUse the dealer's own photo of an asset, say the floor mat they really own. It is chosen at once.
JSON { kind, url }, or multipart kind + file.
curl -X POST "https://listwyse.com/api/v1/studio/branding/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "floorMat",
"url": "https://cdn.example/our-mat.jpg"
}'{ "kind": "floorMat", "url": "https://…", "branding": { … } }/brandingSwitch which saved design new vehicles get.
curl -X PATCH "https://listwyse.com/api/v1/studio/branding" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"chosen": {
"floorMat": "https://…"
}
}'{ "branding": { … } }/branding/designsDelete one saved design. If it was chosen, the next saved one is.
curl -X DELETE "https://listwyse.com/api/v1/studio/branding/designs" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "displayPlate",
"url": "https://…"
}'{ "branding": { … } }/templates/customStart a Build-your-own look from photos of the dealership's building (up to 8).
Storefront, showroom floor, signage. note steers the build. Once built and picked, its templateId works anywhere a look does.
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Our showroom",
"note": "Show the red wall logo behind the car",
"photoUrls": [
"https://…/front.jpg",
"https://…/floor.jpg"
]
}'{
"template": {
"id": "d2343166-…",
"templateId": "custom:d2343166-…",
"name": "Our showroom",
"status": "draft",
"photoUrls": ["https://…"],
"exampleUrls": [],
"selectedUrl": null
}
}/templates/customEvery Build-your-own look on the account, in any status.
status is draft (add photos, then build), generating, examples (pick one), ready (usable) or error. GET /templates lists only ready ones. Add ?providerDealershipId= for one dealership's.
curl "https://listwyse.com/api/v1/studio/templates/custom" \
-H "Authorization: Bearer $LISTWYSE_KEY"{ "templates": [{ "id": "…", "templateId": "custom:…", "status": "ready", … }] }/templates/custom/:idOne Build-your-own look. :id is its id or its templateId.
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{ "template": { "status": "examples", "exampleUrls": ["https://…"], … } }/templates/custom/:id/photosAdd more photos of the building: JSON { urls }, or a multipart file.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://…/entrance.jpg"
]
}'{ "template": { "photoUrls": ["https://…", "https://…"], … } }/templates/custom/:id/photosTake a photo off. The look goes back to draft until built again.
Path
The id from the call that made it.
curl -X DELETE "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://…"
}'{ "template": { "status": "draft", … } }/templates/custom/:id/buildUses creditsPaid AI work: turn the photos into an empty, tidied-up photo of the place with room for a car.
Takes a few minutes. Poll GET /templates/custom/:id until status is examples, then PATCH it with selectedUrl. A running build is returned, not restarted. A ready look needs regenerate: true, and is unusable until a new photo is picked.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID/build" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"regenerate": false
}'{
"template": { "status": "generating", … },
"runId": "run_…",
"operationId": "…",
"started": true
}/templates/custom/:idPick the built photo (selectedUrl, one of exampleUrls) to make the look ready, or rename it / change its note.
Path
The id from the call that made it.
curl -X PATCH "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"selectedUrl": "https://…",
"name": "Our showroom"
}'{ "template": { "status": "ready", "selectedUrl": "https://…", … } }/templates/custom/:id/plateSkip the build: use a finished, empty photo of the dealership. The look is ready at once.
JSON { url, name? }, or multipart file + name.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID/plate" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://…/empty-showroom.jpg",
"name": "Our showroom"
}'{ "template": { "status": "ready", … } }/templates/custom/:idDelete a Build-your-own look. Vehicles already made with it keep their photos.
Path
The id from the call that made it.
curl -X DELETE "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{ "deleted": true, "id": "…" }Step by step: vehicles5 calls
A session is one vehicle. Create it once, then add photos and run tools against it. Use your stock number as externalId so repeats reuse the same session.
/sessionsStart a session for a vehicle. If this key already has that externalId, you get the existing session back (reused: true).
label, externalId, dealershipId, vin and providerDealershipId are all optional. VIN must be 17 characters if you send one. providerDealershipId is a Listwyse dealership on your account (GET /account). The vehicle gets the dealership's branding kit (GET /branding).
curl -X POST "https://listwyse.com/api/v1/studio/sessions" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "2024 Lexus GX 550",
"externalId": "stock-4412",
"dealershipId": "rooftop-14",
"vin": "JTJTBKDJ4R5012345"
}'{
"session": {
"id": "…",
"status": "needs_photos",
"label": "2024 Lexus GX 550",
"externalId": "stock-4412",
"vin": "JTJTBKDJ4R5012345"
},
"reused": false
}/sessionsList recent sessions for this key, newest first.
?externalId=stock-4412 finds one car by your stock number. ?limit= takes 1–100 (default 60). This list is a summary without a status field — use generating and progress to see if work is still running, then GET a single session for full results.
curl "https://listwyse.com/api/v1/studio/sessions" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"sessions": [
{
"id": "…",
"label": "2024 Lexus GX 550",
"externalId": "stock-4412",
"uploadCount": 12,
"imageCount": 4,
"enhancedCount": 0,
"showroomImageCount": 4,
"videoCount": 0,
"generating": true,
"thumbnailUrl": "https://…"
}
]
}/sessions/:idThe one call to poll. Returns the vehicle, original photos, sorted angles, finished photos, films, and anything still in progress.
When status is complete, take URLs from images[].url and videos[].url. Each images[] item is pending, completed, or error. urls lists every version of a shot (re-runs add versions) and url is the current one — the last of urls. progress says which tools are still running.
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"session": {
"id": "…",
"status": "generating",
"label": "2024 Lexus GX 550",
"externalId": "stock-4412",
"vin": "JTJTBKDJ4R5012345",
"slots": { "front": ["https://…"] },
"images": [
{
"presetId": 201,
"title": "Hero",
"status": "completed",
"url": "https://…/v2.jpg",
"urls": ["https://…/v1.jpg", "https://…/v2.jpg"]
}
],
"videos": [{ "requestId": "…", "status": "pending" }],
"pendingImageJobs": [
{ "requestId": "…", "presetId": 201, "title": "Hero" }
],
"progress": {
"sorting": false, "enhance": false, "images": true,
"video": false, "importing": false
},
"operations": [
{ "id": "…", "kind": "images", "status": "running" }
],
"errors": []
}
}/sessions/:idUpdate the display name and/or VIN.
Path
The id from the call that made it.
curl -X PATCH "https://listwyse.com/api/v1/studio/sessions/SESSION_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "GX 550 Overtrail",
"vin": "JTJTBKDJ4R5012345"
}'{ "ok": true, "label": "GX 550 Overtrail", "vin": "JTJTBKDJ4R5012345" }/sessions/:idDelete the session and its results from your account.
Photo and result URLs you already copied keep working.
Path
The id from the call that made it.
curl -X DELETE "https://listwyse.com/api/v1/studio/sessions/SESSION_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{ "ok": true }Step by step: photos, sorting and logo8 calls
Add listing photos, optionally a dealership logo, then sort so Studio knows which angle is which — or send each photo's angle yourself and skip the sort. Branding (wall sign, plate, screen) is optional and only needed for looks that show dealership graphics.
/sessions/:id/photosAdd listing photos from https URLs you already host.
Send { urls } when you don't know the angles, or { photos: [{ url, slot }] } when you do. A photo with a slot goes straight onto that angle (replacing the one there; sign, damage and engine photos add up instead, up to 5, and interior photos up to 20, and each one becomes its own image) and needs no sort; a photo without one needs a sort, and a sort re-labels every photo. The sort asks first whether each photo was taken inside the car: every cabin photo goes under interior (one image each) with three labels (mileage or key info, sunroof, damage) that change how it is made; any other photo gets its angle. Slots: front, frontAngle, side, rear, rearAngle, wheel, headlight, taillight, interior, interiorFront, interiorSide, dashboard, steeringWheel, odometer, trunk, engine and more (an unknown slot returns the full list). You can also send one image as multipart form-data with a file field and an optional slot field. A session holds at most 48 photos; each uploaded file must be under 4MB.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"photos": [
{
"url": "https://cdn.example/front.jpg",
"slot": "front"
},
{
"url": "https://cdn.example/side.jpg",
"slot": "side"
}
]
}'{
"uploads": [
{ "url": "https://…", "name": "front.jpg" }
],
"slots": { "front": ["https://…"], "side": ["https://…"] },
"skipped": 0
}/sessions/:id/photosTake one photo off the vehicle, by its uploads[].url.
The angles that photo filled lose it; the other angles stay sorted, so no new sort is needed.
Path
The id from the call that made it.
curl -X DELETE "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://…/inputs/….jpg"
}'{
"ok": true,
"uploads": [{ "url": "https://…" }],
"slots": { "front": ["https://…"] }
}/sessions/:id/importPull photos from a public listing page instead of sending URLs yourself. Answers at once (202) and imports in the background, so you can start many vehicles in parallel.
The vehicle's status is importing until it finishes (usually 1–3 minutes); poll GET /sessions/:id, then sort. A failed import shows in errors[] (e.g. a site that blocks automated imports — add photos with /photos instead). A VIN on the page is stored when we can read one. A second import on the same vehicle while one is running returns 409.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/import" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"listingUrl": "https://dealer.example/inventory/4412"
}'{
"pending": true,
"runId": "run_…",
"listingUrl": "https://dealer.example/inventory/4412",
"operationId": "…",
"pollIntervalMs": 5000
}/sessions/:id/importCheck one import: still running, the photos it added, or why it failed.
Add ?runId=run_….
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/import" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"vehicleName": "2024 Lexus GX 550",
"label": "2024 Lexus GX 550",
"vin": "JTJTBKDJ4R5012345",
"uploads": [{ "url": "https://…", "name": "listing-1.jpg" }],
"skipped": 0,
"runId": "run_…"
}/sessions/:id/logoSet the dealership logo: a multipart file field, or JSON { url } for a logo you host.
Required before generating a look that shows branding. Send kind wallMount if you already have a pre-rendered wall sign image. Uploaded files must be under 4MB; logos by URL can be up to 12MB.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/logo" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example/logo.png",
"kind": "logo"
}'{
"kind": "logo",
"upload": { "url": "https://…" },
"logoUrl": "https://…"
}/sessions/:id/sortGroup photos by camera angle (front, side, interior, and so on). Required before showroom photos or a film.
Returns 202 while sorting. Poll GET /sessions/:id until status is ready, or GET /sessions/:id/sort. Pass force: true to sort again after you add photos. removeInfographics: true drops price banners burned into the image.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/sort" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"force": false,
"removeInfographics": false
}'{
"pending": true,
"runId": "run_…",
"operationId": "…"
}/sessions/:id/sortCheck whether sorting has finished.
Optional ?runId= from the POST. If sorting is already done, this returns the grouped angles (cached: true) instead of pending.
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/sort" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"pending": true,
"runId": "run_…"
}/sessions/:id/brandingBuild wall-sign, display-plate, and screen graphics from the logo.
You can skip this call — generating showroom photos will build branding first when the look needs it. Returns 409 if photos are still sorting. regenerate: true rebuilds even if assets already exist.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/branding" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"toggles": {
"wallSign": true,
"displayPlate": true,
"monitor": false
},
"templateId": "showroom-set-v2"
}'{
"branding": { "wallSign": true, "displayPlate": true, "monitor": false },
"brandingAssets": { "wallSign": "https://…" },
"built": ["wallSign"]
}VIN and shot planning5 calls
A VIN names the car, unlocks film voiceovers, and lets Smart Capture pick the showroom shots that suit the car (a truck bed, a sunroof, a third row).
/sessions/:id/vinSet the VIN: typed, or read from a photo of the door-frame sticker or windshield plate.
Send { vin }, { photoUrl }, or a multipart file. The photo is not added to the listing photos. An unnamed vehicle is named from the VIN. vin: null means the photo couldn't be read and nothing was saved. checkDigit: false skips the North American check digit for other markets.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/vin" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"photoUrl": "https://cdn.example/door-sticker.jpg"
}'{
"vin": "JTJTBKDJ4R5012345",
"label": "2024 Lexus GX 550 Overtrail",
"decoded": { "year": 2024, "make": "Lexus", "model": "GX 550", "trim": "Overtrail", "fuel": "Gasoline" }
}/vin/:vinWindow-sticker facts for any VIN: specs, standard equipment, EPA fuel economy and NHTSA crash ratings.
From free government sources. Equipment lists only what the decode marks as standard.
Path
vin
curl "https://listwyse.com/api/v1/studio/vin/1HGCV1F34PA012345" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"facts": {
"vin": "JTJTBKDJ4R5012345",
"year": 2024, "make": "Lexus", "model": "GX 550",
"engine": "3.4L 6-cylinder turbo, 349 hp",
"drivetrain": "4WD",
"equipment": [{ "title": "Safety", "items": ["…"] }],
"fuelEconomy": { "unit": "MPG", "combined": 17, "city": 15, "highway": 21 },
"safety": { "overall": 5 }
}
}/sessions/:id/shot-planSmart Capture: the showroom shots picked for this car from its VIN, and the photo angle each one needs.
Showroom photo runs use this list when shotMode is smart (the default). A shot whose needsPhoto angle has no photo is swapped for the next best. Built on the first call and kept; refresh: true rebuilds it. plan is null until the car has a VIN.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/shot-plan" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"refresh": false
}'{
"plan": {
"vin": "…",
"name": "2024 Ram 1500 Limited",
"shots": [
{ "key": "01", "title": "Worm's-eye front 3/4", "reason": null, "needsPhoto": "frontAngle" },
{ "key": "51", "title": "Truck bed", "reason": "Pickup: the bed", "needsPhoto": "bed" }
],
"highlights": ["Truck bed"]
},
"shotMode": "smart"
}/shot-planThe same plan for a VIN before the car is photographed, so you know which photos to take. Nothing is saved.
Add ?vin=….
curl "https://listwyse.com/api/v1/studio/shot-plan" \
-H "Authorization: Bearer $LISTWYSE_KEY"{ "plan": { "vin": "…", "shots": [ … ], "highlights": [ … ] } }/sessions/:id/check-shotCheck that a photo shows the angle it was taken for, or ask which angle it shows.
url must be one of the vehicle's photos. Leave slot out to only ask what it shows. verdict is match, mismatch or unsure. Saved on the vehicle under shotChecks; asking again returns the saved check.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/check-shot" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://…/inputs/….jpg",
"slot": "frontAngle"
}'{
"check": {
"expected": "frontAngle",
"verdict": "mismatch",
"seen": "side",
"note": "Side profile, not a three-quarter view."
}
}Step by step: retouch, showroom photos and film8 calls
After photos are sorted (status ready), run any mix of these. Each start call returns 202 and a runId. Poll the matching GET with that runId until jobs are listed or the film has a requestId — then keep polling the session for finished URLs.
/sessions/:id/enhanceNo background: retouch each original photo once (lighting, reflections, clutter), keeping its framing.
Does not require a look. Photos must already be on the session; ones not yet sorted are sorted first. Each result keeps its photo's shape. sourceUrls (from uploads[].url) redoes just those photos. configId (the retired Enhance Configs) is ignored.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/enhance" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceUrls": [
"https://…/inputs/….jpg"
]
}'{
"preparing": true,
"runId": "run_…",
"operationId": "…",
"jobs": [],
"pollIntervalMs": 2500
}/sessions/:id/enhanceWait until each enhanced photo has been queued.
runId is required. When completed is true, jobs[] has a requestId per photo. Finished files then appear on GET /sessions/:id under images. Add ?runId=run_….
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/enhance" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"completed": true,
"jobs": [
{ "presetId": 29001, "title": "Enhance — Front", "requestId": "…" }
]
}/sessions/:id/imagesGenerate showroom photos for a look. Branding for that look is built first if needed.
Photos must be sorted. Looks that are video-only return 400 — use /video instead. Optional presetIds: [201, 203] makes only those shots; to re-make shots you already have, send their images[].presetId and templateId can be left out. Each re-run adds a version. With presetIds, updateTwins: true also puts each new photo on the other cars of the dealership sharing that photo (same photo files, look and settings).
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/images" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "panel-showroom-catalogue"
}'{
"preparing": true,
"runId": "run_…",
"templateId": "panel-showroom-catalogue",
"jobs": [],
"pollIntervalMs": 2500
}/sessions/:id/imagesWait until showroom shots are queued.
runId is required. When jobs[] is filled, poll the session until those photos show status completed. Add ?runId=run_….
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/images" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"templateId": "panel-showroom-catalogue",
"jobs": [{ "presetId": 201, "title": "Hero", "requestId": "…" }],
"brandingAssets": { "wallSign": "https://…" }
}/sessions/:id/videoStart the 16:9 film for a look (15 seconds; 10 for a custom template). Missing showroom photos for that look are generated first.
templateId is required. imageTemplateId is accepted as the same field. Photos must be sorted. Looks without a film recipe return 400. templateId "enhance" makes the film from the car's enhanced photos (run POST /enhance first; 409 until there are some).
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/video" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "panel-showroom-catalogue"
}'{
"preparing": true,
"runId": "run_…",
"templateId": "panel-showroom-catalogue",
"pollIntervalMs": 2500
}/sessions/:id/videoWait until the film has been submitted, then until a url is present.
runId is required. queued + requestId means the film is rendering. completed + url means you can download it. The session videos[] list updates at the same time. Add ?runId=run_….
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/video" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"queued": true,
"completed": false,
"requestId": "…",
"templateId": "panel-showroom-catalogue"
}/sessions/:id/voiceoverVoice a finished film: a script written from the car's VIN, read over the film.
requestId is the film's videos[].requestId. The car needs a VIN (send vin here to set one). Returns 202 with a runId.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/voiceover" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "…",
"vin": "JTJTBKDJ4R5012345"
}'{
"pending": true,
"runId": "run_…",
"pollIntervalMs": 2500
}/sessions/:id/voiceoverWait for the voiced film.
completed + url is the film with the voiceover. It also appears on the session as videos[].voiceover. Add ?runId=run_….
Path
The id from the call that made it.
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/voiceover" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"completed": true,
"url": "https://…/voiced.mp4",
"script": "…"
}Step by step: versions2 calls
Every re-run of a shot adds a version; images[].url is the current one. Pick another version, or delete ones you don't want.
/sessions/:id/versionsMake one of a shot's versions (images[].urls) its current photo.
Path
The id from the call that made it.
curl -X POST "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/versions" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"presetId": 201,
"url": "https://…/v1.jpg"
}'{ "ok": true, "presetId": 201, "url": "https://…/v1.jpg" }/sessions/:id/versionsDelete generated photos. A shot with no versions left drops out of images.
Path
The id from the call that made it.
curl -X DELETE "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/versions" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [
{
"presetId": 201,
"url": "https://…/v1.jpg"
}
]
}'{ "ok": true, "removed": 1 }Step by step: one job1 call
Use this when you have a requestId from jobs[] and want that one photo’s provider result. For day-to-day polling, GET /sessions/:id is enough.
/sessions/:id/jobs/:requestIdInspect one photo job, including the finished file or the error.
Job status uses succeeded / failed (not completed / error like session.images[]).
Path
The id from the call that made it.
requestId
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/jobs/REQUEST_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"job": {
"requestId": "…",
"operationId": "…",
"kind": "image",
"presetId": 201,
"title": "Hero",
"status": "succeeded",
"result": { "images": [{ "url": "https://…" }] }
}
}