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 (
code, sayDEALER-01). From then on you name the dealer by that code, asdealerCode. We also give each dealer our own id (dealerId); you can use that instead if you prefer. (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": "Example Motors", "code": "DEALER-01" }' - 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": "PHOTO-1001", "url": "https://cdn.example.com/shoots/88231.jpg", "stockNumber": "A1234", "dealerCode": "DEALER-01" }' - 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/PHOTO-1001" \ -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/PHOTO-1001finds the photo you sent asPHOTO-1001. Encode any characters a URL can't carry (encodeURIComponentin JavaScript).
How it works
How the API works
What the API is made of, how a request turns into a finished photo, and what it costs.
Structure
Account
Your company
Holds your credits and your API key, with its webhook URL.
Dealer
Example Motors
A dealership you shoot for, with its own settings.
Vehicle
Stock A1234 / VIN 1HGCV1F34PA012345
One car at that dealer, named by its stock number or its VIN. Its first photo makes it.
Photo
photoId: PHOTO-1001
Photo
photoId: PHOTO-1002
Photo
photoId: PHOTO-1003
There's no call to make a vehicle. Every photo names its car by stock number (stockNumber) or VIN (vin); send either, or both. The first photo with a new one makes the car, and every later photo with the same stock number or VIN, at the same dealer, joins it.
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.
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.
As part of quality control we analyse each photo and make improvements to keep result to a high standard.
- While this happens, the photo's status is
quality_assurance. It can add a few minutes to a photo. - 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.
Credits
You pay for photos you get back finished, and for the assets you have us make. 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.
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.
Making a dealer's assets costs 1 credit for each branding design or custom background build, held the same way and given back if it fails.
GET /usage lists every charge, newest first, with totals: narrow it to one dealer and a month to bill that dealer.
Guides
Guides
Each part of an integration in turn: sending, hearing back, settings, a dealer's assets, and what to do when something goes wrong.
Sending photos
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, branding as JSON text.
curl -X POST "https://listwyse.com/api/v1/studio/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-F "file=@88231.jpg" \
-F "photoId=PHOTO-1001" \
-F "stockNumber=A1234" \
-F "dealerCode=DEALER-01" \
-F "angle=frontAngle" \
-F "outputFormat=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/PHOTO-1001.
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 says which car the photo is of, and dealerCode 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 saved settings. Any send can change them for its own photos: the controls (cleanup, outputFormat and the rest), branding (the dealer's plate, mat or screen) and background (no background, or a background). Settings has them all.
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. - Up to 300 of your account's photos are made at once. Send as many as you like: the rest wait,
queued, and start as others finish. Nothing is turned down. - A key can make 600 requests a minute. Past that you get
429 RATE_LIMITEDwith aRetry-Afterheader saying how many seconds to wait. Use webhooks rather than asking again and again.
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": "DEALER-01",
"branding": [
"plate",
"floorMat",
"screen"
],
"photos": [
{
"photoId": "PHOTO-1001",
"url": "https://cdn.example.com/shoots/88231.jpg"
},
{
"photoId": "PHOTO-1002",
"url": "https://cdn.example.com/shoots/88232.jpg"
},
{
"photoId": "PHOTO-1003",
"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, a branding design or a custom background finishes.
The webhook belongs to your API key: events for the photos sent, and the assets started, 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 |
| branding.completed | A branding design is saved and in use. | branding, the item, and dealerId |
| branding.failed | A branding design couldn't be made (its credit was given back). | branding, the item, dealerId and error |
| background.completed | A custom background is built: pick one of exampleUrls. | template |
| background.failed | A custom background couldn't be built (its credit was given back). | template, with its error |
| 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": "PHOTO-1001",
"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
}
}Settings
Settings decide how a photo comes out. There are three kinds, and the photo, vehicle and redo calls take all three.
| Kind | On a call | What it decides | Left out of a call |
|---|---|---|---|
| Controls | cleanup, outputFormat… | How clean the car and the lot get, the framing, and the file you get back. | The dealer's saved settings |
| Branding | branding | The dealer's plate, floor mat and screen. | None |
| Backgrounds | background | No background (the photo's own surroundings), or a background: one of our presets or a custom one. | The dealer's saved settings |
Controls
Controls are the sliders (how far the retouch goes) and the file you get back. Choose them on each call, or save them once for a dealer and leave them out. They have the same names either way, and a call's own choice always wins.
Manual: on the call
Send any of the controls with the photos. They apply to that call's photos only and aren't saved. Send just the ones you want to change: the rest come from the dealer's saved settings.
{
"url": "https://cdn.example.com/shoots/88231.jpg",
"stockNumber": "A1234",
"dealerCode": "DEALER-01",
"cleanup": 1,
"reflections": 0,
"outputFormat": "png",
"aspectRatio": "4:3"
}| Control | Choices | Default | What it does |
|---|---|---|---|
| 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. |
| outputFormat | jpeg · png · webp | jpeg | The file type photos come back as. |
| aspectRatio | match · 4:3 · 3:2 · 16:9 | match | The shape photos come back in. match keeps each photo's own shape; the others crop the finished photo to that shape, keeping the car in frame. |
Sliders are step numbers. GET /dealers/:id/settings lists every step and its name under options. The Default column is Listwyse's default.
Dealer settings: saved once
Save controls for a dealer and every call for that dealer uses them without saying. There's no separate settings id: the dealerId (or dealerCode) a call already sends picks the dealer's saved settings.
Each control comes from the first place that has it:
- The call itself (manual).
- The dealer's own settings:
PATCH /dealers/:id/settings. The first change gives the dealer its own copy (what it followed, plus your change), so later changes to your account's settings don't reach it. - Your account's settings:
PATCH /dealers/default/settings. Every dealer follows them until it has its own. - Listwyse's defaults.
Saved settings use the same names as a call, so what GET /dealers/:id/settings returns can be sent on a call as it is. They also hold a background. Branding isn't saved: ask for it on each call. The same settings show in the Listwyse app.
curl -X PATCH "https://listwyse.com/api/v1/studio/dealers/DEALER-01/settings" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"reflections": 1,
"outputFormat": "png"
}'Branding
Puts the dealer's own branding on its photos. It's off unless a call asks: send branding with the items you want.
"branding": ["plate", "floorMat", "screen"]| Item | What it adds |
|---|---|
| plate | The dealer's plate on the car, front or back, when the photo shows that plate spot. |
| floorMat | The dealer's mat in the driver's footwell, when the photo shows the footwell. |
| screen | The dealer's picture on the centre screen, when the photo shows the screen. |
Only where it fits
- Set up once per dealer. The plate, mat and screen come from the dealer's logo, or from photos of its own: Asset creation shows how. Asking for branding before the dealer has a logo is turned down with
400 BRANDING_NOT_SET_UP. - On a redo, send
brandingto change it ([]turns it off), or leave it out to keep what the photo had.
Backgrounds
A photo comes back with no background or with a background. Send background on a call, or save one in the dealer's settings. Listwyse's default is no background.
| Option | Send | What you get |
|---|---|---|
| No background | "keep" | The photo keeps its own surroundings, retouched where it was taken, as far as Clean-up and Background tidy say. |
| A background | A background's id | The car is placed on a premade background, in the same view. You still get one finished photo for each photo you send. |
A background is one of our presets, or a custom one made for the dealer.
Presets
| Background id | Name | What it is |
|---|---|---|
| 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. |
Custom backgrounds
The best background is the dealer's own showroom, and we encourage it: make one in Asset creation, from photos of the building or from an empty photo of it you already have. Once it's ready, its id (starting custom:) works just like a preset, and GET /templates lists it beside ours.
A custom background is one dealer's own or universal, chosen when it's made. A dealer's photos can use its own and the universal ones, never another dealer's showroom; your account's default settings, only universal ones. GET /templates?dealerCode=… lists exactly what a dealer can use.
A close-up a background has no shot for (a badge, say) comes back with no background instead, and its note says so.
Asset creation
A dealer's branding and its own custom background are made once, then used on every photo that asks for them.
| Asset | Made from | Costs | Used when |
|---|---|---|---|
| Plate, screen, floor mat | The dealer's logo (we design each one), or a photo of the dealer's own | 1 credit per design; a photo of their own is free | A call sends branding |
| A custom background (the dealer's showroom) | Photos of the building (we build an empty showroom), or an empty photo you already have | 1 credit per build; a photo of your own is free | A call's background is its id |
Name the dealer as on the photo calls, with dealerId or dealerCode; a dealership's own account leaves it out. A design or build that fails gives its credit back, and asking for one that's already being made isn't charged again.
Branding
- Add the logo:
POST /branding/logo, as a link or a file. - Make the items:
POST /branding/designsdesigns a plate, a screen picture and a floor mat from the logo, 1 credit each, in a minute or two. Or, if the dealer has a real one, send a photo of it withPOST /branding/photos, free. - Check them: a
branding.completedwebhook arrives as each item lands (or askGET /brandinguntil each item'sgeneratingis false).chosenUrlis what photos get;PATCH /brandingswitches to another saved design. - Use them: send
brandingon a photo or vehicle call.
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"
],
"dealerCode": "DEALER-01"
}'A custom background
Made from the dealer's own showroom, so its cars are shown where they're sold.
- Start it:
POST /templates/customwith up to 8 photos of the building: the showroom floor, the walls, the signage. Anotesays what to keep (“the red wall logo behind the car”). Name the dealer it's for (only its photos can use it), or senduniversal: trueso every dealer on your account can. - Build it:
POST /templates/custom/:id/build, 1 credit, a few minutes. We turn the photos into an empty, tidied-up showroom with room for a car. - Pick the photo: a
background.completedwebhook arrives when it's built (or askGET /templates/custom/:iduntilstatusisexamples), thenPATCH /templates/custom/:idwith one ofexampleUrlsasselectedUrl. The background is ready. - Use it: send its
templateId(custom:…) asbackground, or save it as the dealer's background in its settings.
Already have a clean photo of the empty showroom? Skip steps 2 and 3: POST /templates/custom/:id/plate makes it the background, ready at once and free.
| status | What it means |
|---|---|
| draft | Add photos, then build it. |
| generating | Being built. |
| examples | Built: pick one of exampleUrls. |
| ready | Usable as a background. |
| error | The build failed (its credit was given back): error says why. Build it again. |
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID/build" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{}'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 | That background isn't a preset or a custom background this dealer can use. | Use "keep" or an id from GET /templates?dealerCode=…. |
| DEALER_REQUIRED | 400 | You shoot for dealers and the call didn't name one. | Send dealerCode. |
| BRANDING_NOT_SET_UP | 400 | branding asked for, and the dealer has no logo yet. | Add it with POST /branding/logo, then send again. |
| 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. |
| RATE_LIMITED | 429 | More than 600 requests in a minute from one key. | Wait the seconds in the Retry-After header, then send it again. |
| 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 background 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 Example Motors”). 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.
match, 4:3, 3:2 or 16:9
The shape of url: match is the photo's own shape; the others are cropped to that shape.
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" (no background), or the background it was put on.
list
The dealer branding asked for (plate, floorMat, screen). Each is only added where the photo shows its spot.
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": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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": "Example Motors",
"code": "DEALER-01",
"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, branding.completed, branding.failed, background.completed, background.failed 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.
branding, string, string or null
On branding.completed and branding.failed: the dealer's branding (as GET /branding), the item that finished, and its dealer. A failed one also has data.error.
custom background
On background.completed (built: pick one of exampleUrls) and background.failed (its error says why).
Example
{
"id": "e2b1c0de-…",
"event": "photo.completed",
"createdAt": "2026-10-24T15:05:40.000Z",
"data": {
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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, branding 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" for no background (the photo keeps its own surroundings, retouched where it was taken), or a background's id (GET /templates): one of our presets or a custom one. Defaults to the dealer's setting.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number (GET /dealers/:id/settings lists them).
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 file type photos come back as: jpeg, png or webp.
The shape photos come back in: "match" keeps each photo's own shape; 4:3, 3:2 or 16:9 crop the finished photo to that shape, keeping the car in frame.
Dealer branding to add: any of "plate", "floorMat", "screen". Each is only added where the photo shows its spot (a plate where a plate goes, a mat where the driver's footwell shows). Leave it out for none. Needs the dealer's logo or designs (the branding calls).
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": "PHOTO-1001",
"url": "https://cdn.example.com/shoots/88231.jpg",
"stockNumber": "A1234",
"dealerCode": "DEALER-01"
}'{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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/PHOTO-1001" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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, the branding 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" for no background (the photo keeps its own surroundings, retouched where it was taken), or a background's id (GET /templates): one of our presets or a custom one. Defaults to the dealer's setting.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number (GET /dealers/:id/settings lists them).
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 file type photos come back as: jpeg, png or webp.
The shape photos come back in: "match" keeps each photo's own shape; 4:3, 3:2 or 16:9 crop the finished photo to that shape, keeping the car in frame.
Dealer branding to add: any of "plate", "floorMat", "screen". Each is only added where the photo shows its spot (a plate where a plate goes, a mat where the driver's footwell shows). Leave it out for none. Needs the dealer's logo or designs (the branding calls).
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/PHOTO-1001/redo" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"cleanup": 1,
"outputFormat": "png"
}'{
"photo": {
"id": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"photoId": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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/PHOTO-1001" \
-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" for no background (the photo keeps its own surroundings, retouched where it was taken), or a background's id (GET /templates): one of our presets or a custom one. Defaults to the dealer's setting.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number (GET /dealers/:id/settings lists them).
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 file type photos come back as: jpeg, png or webp.
The shape photos come back in: "match" keeps each photo's own shape; 4:3, 3:2 or 16:9 crop the finished photo to that shape, keeping the car in frame.
Dealer branding to add: any of "plate", "floorMat", "screen". Each is only added where the photo shows its spot (a plate where a plate goes, a mat where the driver's footwell shows). Leave it out for none. Needs the dealer's logo or designs (the branding calls).
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": "DEALER-01",
"branding": [
"plate",
"floorMat",
"screen"
],
"photos": [
{
"photoId": "PHOTO-1001",
"url": "https://cdn.example.com/shoots/88231.jpg"
},
{
"photoId": "PHOTO-1002",
"url": "https://cdn.example.com/shoots/88232.jpg"
},
{
"photoId": "PHOTO-1003",
"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": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": null,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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": "PHOTO-1001",
"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",
"aspectRatio": "match",
"edited": true,
"angle": "frontAngle",
"angleSource": "given",
"background": "keep",
"branding": [],
"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=DEALER-01" \
-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": "Example Motors",
"code": "DEALER-01",
"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": "Example Motors",
"code": "DEALER-01"
}'{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Example Motors",
"code": "DEALER-01",
"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/DEALER-01" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Example Motors",
"code": "DEALER-01",
"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/DEALER-01" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Motors Downtown"
}'{
"dealer": {
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"name": "Example Motors",
"code": "DEALER-01",
"createdAt": "2026-10-24T14:40:00.000Z"
}
}/dealers/:idDelete a dealer
Takes a dealer off your account: it leaves GET /dealers and calls can't name it any more. Its cars and finished photos stay, and their files keep working. Its code is freed for a new dealer.
When to use it: You stop shooting for a dealership.
Path
Our dealer id, or your code.
Errors
curl -X DELETE "https://listwyse.com/api/v1/studio/dealers/DEALER-01" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"deleted": true,
"id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
}/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/DEALER-01/settings" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"settings": {
"background": "keep",
"outputFormat": "jpeg",
"aspectRatio": "match",
"cleanup": 1,
"backgroundTidy": 1,
"reflections": 1,
"framing": 1
},
"inherited": false,
"options": {
"background": [
"keep",
"grey-studio-catalogue",
"panel-showroom-catalogue",
"…"
],
"outputFormat": [
"jpeg",
"png",
"webp"
],
"aspectRatio": [
"match",
"4:3",
"3:2",
"16:9"
],
"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" (no background) or a background's id.
Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number (GET /dealers/:id/settings lists them).
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 file type photos come back as: jpeg, png or webp.
The shape photos come back in: "match" keeps each photo's own shape; 4:3, 3:2 or 16:9 crop the finished photo to that shape, keeping the car in frame.
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/DEALER-01/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",
"aspectRatio": "match",
"cleanup": 1,
"backgroundTidy": 1,
"reflections": 1,
"framing": 1
},
"inherited": false,
"options": {
"background": [
"keep",
"grey-studio-catalogue",
"panel-showroom-catalogue",
"…"
],
"outputFormat": [
"jpeg",
"png",
"webp"
],
"aspectRatio": [
"match",
"4:3",
"3:2",
"16:9"
],
"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": [
"…"
]
}
}
}
}Asset creation
A dealer's own branding and custom backgrounds, made once and used on every photo that asks. Making one costs 1 credit, given back if it fails; using a photo of your own is free.
/brandingSee a dealer's branding
The dealer's logo and, for each branding item (plate, screen, mat), its saved designs and which one is used.
When to use it: Checking what a dealer's photos will get, or waiting for a design to finish.
Query
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.
The answer: Each item's chosenUrl is the design photos get. generating is true while a design is being made; error says why one failed. uploaded marks a photo of the dealer's own item.
Errors
curl "https://listwyse.com/api/v1/studio/branding?dealerCode=DEALER-01" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"branding": {
"dealership": {
"name": "Example Motors",
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
},
"logoUrl": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/logo.png",
"assets": {
"displayPlate": {
"chosenUrl": "https://…/plate.png",
"designs": [
{
"url": "https://…/plate.png",
"uploaded": false
}
],
"generating": false,
"runId": null,
"error": null
},
"monitor": {
"chosenUrl": null,
"designs": [],
"generating": true,
"runId": "run_…",
"error": null
},
"floorMat": {
"chosenUrl": "https://…/our-mat.jpg",
"designs": [
{
"url": "https://…/our-mat.jpg",
"uploaded": true
}
],
"generating": false,
"runId": null,
"error": null
}
}
}
}/branding/logoSet a dealer's logo
The logo every design is made from. Send JSON with a link, or multipart/form-data with the logo as a file field. Transparent areas are flattened onto white.
When to use it: Setting a dealer up. Designs made from an earlier logo stay in use until you make new ones.
Body (JSON, or the same fields as a form with a file)
A public https link to the logo. Or send it as a multipart file field.
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.
Errors
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.com/dealer/logo.png",
"dealerCode": "DEALER-01"
}'{
"branding": {
"dealership": {
"name": "Example Motors",
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
},
"logoUrl": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/logo.png",
"assets": "…"
}
}/branding/designsUses creditsDesign branding items
Makes a plate, a screen picture and a floor mat from the logo with AI: one design each, used as soon as it lands. Each design costs 1 credit.
When to use it: The dealer has a logo but no plate, screen or mat of its own to photograph.
Body
Which items to design. All three if left out.
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.
What happens
- We hold 1 credit for each item and answer at once, with those items generating.
- Each design takes a minute or two. It's saved, and used, as soon as it lands.
- A design that can't be made gives its credit back, and its error says why.
The answer: A branding.completed (or branding.failed) webhook arrives as each item finishes. Without webhooks, ask GET /branding until each item's generating is false.
Errors
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"
],
"dealerCode": "DEALER-01"
}'{
"branding": {
"dealership": {
"name": "Example Motors",
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
},
"logoUrl": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/logo.png",
"assets": {
"displayPlate": {
"generating": true,
"runId": "run_…"
},
"…": "…"
}
},
"credits": {
"pricePerDesign": 1,
"held": 3,
"balance": 38.9
}
}/branding/photosUse the dealer's own item
A photo of a branding item the dealer really has, say its own floor mat, used instead of a design. Free. Send JSON with a link, or multipart/form-data with kind and a file field.
When to use it: The dealer has a real plate, mat or screen picture and wants exactly that on its photos.
Body (JSON, or the same fields as a form with a file)
"displayPlate" (the plate), "monitor" (the screen) or "floorMat".
A public https link to a photo of the dealer's own item. Or a multipart file field.
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.
The answer: It's saved and used at once.
Errors
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.com/dealer/our-mat.jpg",
"dealerCode": "DEALER-01"
}'{
"kind": "floorMat",
"url": "https://…/our-mat.jpg",
"branding": "…"
}/brandingChoose which design is used
Switches an item to another of its saved designs.
When to use it: A dealer has more than one design of an item and prefers an earlier one.
Body
For each item to switch, the url of one of its saved designs (GET /branding lists them).
—
—
—
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.
Errors
curl -X PATCH "https://listwyse.com/api/v1/studio/branding" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"chosen": {
"floorMat": "https://…/our-mat.jpg"
},
"dealerCode": "DEALER-01"
}'{
"branding": {
"dealership": {
"name": "Example Motors",
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
},
"logoUrl": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/logo.png",
"assets": {
"displayPlate": {
"chosenUrl": "https://…/plate.png",
"designs": [
{
"url": "https://…/plate.png",
"uploaded": false
}
],
"generating": false,
"runId": null,
"error": null
},
"monitor": {
"chosenUrl": null,
"designs": [],
"generating": true,
"runId": "run_…",
"error": null
},
"floorMat": {
"chosenUrl": "https://…/our-mat.jpg",
"designs": [
{
"url": "https://…/our-mat.jpg",
"uploaded": true
}
],
"generating": false,
"runId": null,
"error": null
}
}
}
}/branding/designsDelete a saved design
Removes one saved design. If it was the one in use, the next saved one is used instead.
Body
"displayPlate" (the plate), "monitor" (the screen) or "floorMat".
The saved design's url.
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.
Errors
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://…/plate.png",
"dealerCode": "DEALER-01"
}'{
"branding": {
"dealership": {
"name": "Example Motors",
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77"
},
"logoUrl": "https://qzemyvwjmbhjilubfpzh.supabase.co/storage/v1/object/public/photos/…/logo.png",
"assets": {
"displayPlate": {
"chosenUrl": "https://…/plate.png",
"designs": [
{
"url": "https://…/plate.png",
"uploaded": false
}
],
"generating": false,
"runId": null,
"error": null
},
"monitor": {
"chosenUrl": null,
"designs": [],
"generating": true,
"runId": "run_…",
"error": null
},
"floorMat": {
"chosenUrl": "https://…/our-mat.jpg",
"designs": [
{
"url": "https://…/our-mat.jpg",
"uploaded": true
}
],
"generating": false,
"runId": null,
"error": null
}
}
}
}/templates/customStart a custom background
A background of the dealer's own showroom, made from photos of the building. Name the dealer it's for, and only that dealer's photos can use it; or send universal: true and every dealer on your account can. It starts as a draft: add photos, then build it (or use an empty photo of the place you already have).
When to use it: A dealer wants its cars shown in its own showroom rather than on one of our presets.
Body
A name for the background.
Anything the build should keep, e.g. "show the red wall logo behind the car".
Photos of the building: the showroom floor, the storefront, a branded wall. Up to 8.
The dealer it's for: only that dealer's photos can use it. A dealership's own account leaves it out.
Or your own code for that dealer.
Instead of a dealer: true lets every dealer on your account use it. An account that shoots for dealers sends a dealer or this.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"photoUrls": [
"https://cdn.example.com/dealer/floor.jpg",
"https://cdn.example.com/dealer/wall.jpg"
],
"dealerCode": "DEALER-01"
}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "draft",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/customList your custom backgrounds
Every custom background made for your account, in any status. GET /templates lists only the ready ones, beside our presets.
Query
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.
The answer: status is draft (add photos, then build), generating, examples (pick one), ready (usable as a background) or error. With a dealer named, you get the ones it can use: its own and the universal ones (universal: true).
Errors
curl "https://listwyse.com/api/v1/studio/templates/custom?dealerCode=DEALER-01" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"templates": [
{
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "examples",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
]
}/templates/custom/:idCheck a custom background
One custom background, by its id or its templateId.
When to use it: Waiting for a build to finish.
Path
The background's id, or its templateId (custom:…).
The answer: Once status is examples, exampleUrls holds the built photos to pick from.
Errors
curl "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "examples",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/custom/:id/photosAdd photos of the building
More photos for a custom background, up to 8 in all. Send JSON with links, or multipart/form-data with a file field.
Path
The background's id, or its templateId.
Body (JSON, or the same fields as a form with a file)
More photos of the building (up to 8 in all). Or a multipart file field.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://cdn.example.com/dealer/entrance.jpg"
]
}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "draft",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/custom/:id/photosRemove a photo of the building
Takes one photo off a custom background. It goes back to draft until it's built again.
Path
The background's id, or its templateId.
Body
One of the background's photoUrls.
Errors
curl -X DELETE "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID/photos" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://…/wall.jpg"
}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "draft",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/custom/:id/buildUses creditsBuild a custom background
Turns the photos of the building into an empty, tidied-up photo of the place with room for a car. Costs 1 credit.
When to use it: The background has its photos and is ready to be made.
Path
The background's id, or its templateId.
Body
Build a ready background again from scratch. It can't be used until a new photo is picked.
What happens
- We hold 1 credit and answer at once, with the background generating.
- The build takes a few minutes. A background already building is handed back, not started again (and not charged).
- It ends at examples, and a background.completed webhook carries it: pick one of exampleUrls with PATCH /templates/custom/:id, and the background is ready. A build that fails gives its credit back and sends background.failed.
Errors
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID/build" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "generating",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [],
"selectedUrl": null,
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
},
"runId": "run_…",
"operationId": "…",
"started": true,
"credits": {
"pricePerBuild": 1,
"held": 1,
"balance": 40.9
}
}/templates/custom/:idPick the built photo
Makes the background ready with one of its built photos (selectedUrl). Also renames it or changes its note.
Path
The background's id, or its templateId.
Body
One of exampleUrls: the background is ready to use.
—
—
The answer: Once status is ready, its templateId works as background on any photo or vehicle call.
Errors
curl -X PATCH "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"selectedUrl": "https://…/built-1.jpg"
}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "ready",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": "https://…/built-1.jpg",
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/custom/:id/plateUse your own empty photo
Skips the build: a finished, empty photo of the place you already have becomes the background, ready at once. Free. Send JSON with a link, or multipart/form-data with a file field.
When to use it: You already have a clean photo of the empty showroom.
Path
The background's id, or its templateId.
Body (JSON, or the same fields as a form with a file)
A finished, empty photo of the place, with room for a car. Or a multipart file field.
—
Errors
curl -X POST "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID/plate" \
-H "Authorization: Bearer $LISTWYSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example.com/dealer/empty-showroom.jpg"
}'{
"template": {
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"templateId": "custom:3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13",
"name": "Example Motors showroom",
"note": "Show the red wall logo behind the car",
"status": "ready",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"universal": false,
"providerDealershipId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"photoUrls": [
"https://…/floor.jpg",
"https://…/wall.jpg"
],
"exampleUrls": [
"https://…/built-1.jpg"
],
"selectedUrl": "https://…/empty-showroom.jpg",
"error": null,
"createdAt": "2026-10-26T09:12:00.000Z",
"updatedAt": "2026-10-26T09:16:40.000Z"
}
}/templates/custom/:idDelete a custom background
Removes a custom background. Photos already made on it keep their files.
Path
The background's id, or its templateId.
Errors
curl -X DELETE "https://listwyse.com/api/v1/studio/templates/custom/LOOK_ID" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"deleted": true,
"id": "3b5f0a1e-9c2d-4e7f-8a61-2d4c9b0e7f13"
}Webhooks
Where we tell you about finished photos, branding designs and custom backgrounds. 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",
"branding.completed",
"branding.failed",
"background.completed",
"background.failed",
"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 backgrounds
Your key, your credits and what you've spent them on, and the backgrounds a photo can be put on.
/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": "Example Photo Co",
"prefix": "sk_live_1d8d34c6…",
"webhookUrl": "https://api.example.com/listwyse/events"
},
"organization": {
"id": "…",
"name": "Example Photo Co",
"kind": "provider"
},
"dealerships": [
{
"id": "d89df6f9-…",
"name": "Example Motors"
}
],
"credits": {
"balance": 41.9,
"pricePerPhoto": 0.1
},
"limits": {
"photosPerVehicle": 60,
"photosPerVehicleCall": 60,
"uploadBytes": 4194304
}
}/usageSee what you've spent
Every charge on your credits, newest first, 50 at a time: photos, branding designs and custom background builds made through the API, and Vehicle Tokens spent in the Listwyse app (the same balance). Narrow it by dealer and dates.
When to use it: Billing your dealers for a month, or checking what a balance went on.
Query
Only this dealer's charges (photos, designs and builds made through the API).
Or the dealer by your code.
From this date or time (2026-10-01, UTC).
Up to, not including, this date or time.
How many (1–100, default 50).
Paging: the createdAt of the last item you have; you get older ones.
The answer: Each charge has its credits and status: held (still being made), charged, or refunded (refunded says how much came back). totals adds up every charge the filters match, not just this page. next is set when there are more: send it back as before.
Errors
curl "https://listwyse.com/api/v1/studio/usage?dealerCode=DEALER-01&from=2026-10-01&to=2026-11-01" \
-H "Authorization: Bearer $LISTWYSE_KEY"{
"usage": [
{
"id": "8c0f2a51-…",
"createdAt": "2026-10-24T15:02:11.000Z",
"type": "photo",
"description": "Partner API photo",
"credits": 0.1,
"refunded": 0,
"status": "charged",
"dealerId": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
"vehicleId": "0f3e6d2c-41a7-4a0b-8f1e-3b9d2c7a6e55",
"photoId": "5b0c1e8a-7f0e-4b7a-9a43-0a1c3f2d9e11",
"stockNumber": "A1234"
}
],
"totals": {
"charged": 41.3,
"held": 0.4,
"refunded": 1.2
},
"next": "2026-10-24T15:02:11.000Z"
}/templatesList the backgrounds
Every background a photo can be put on: our presets, and the ready custom ones made for your account. Name a dealer to get only what its photos can use: the presets, its own custom backgrounds and the universal ones.
Query
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.
The answer: Send one's id as background. A custom one's dealerId says whose it is (null with universal: true). Skip ones 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": "Example Motors showroom",
"builtIn": false,
"videoOnly": false
}
]
}