Listwyse API

Send the photos you shoot. Get finished listing photos back.

https://listwyse.com/api/v1/studioOpenAPI file

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. 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. 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. 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 photo queued.

    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. 4

    Get the finished photo

    A few minutes later the photo is completed and url is the finished file. Ask for it by your photoId, 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"
That's the whole loop. The rest of these docs is detail on those four steps.

Keys and requests

  • Every call goes to https://listwyse.com/api/v1/studio followed 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 the X-Request-Id header). Quote it if you ask us about a call.
  • Your own ids work in paths: GET /photos/PMP-88231 finds the photo you sent as PMP-88231. Encode any characters a URL can't carry (encodeURIComponent in 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.

your API key

Dealer

Riverton Acura

A dealership you shoot for, with its own settings.

dealerCode: RIV-014

Vehicle

Stock A1234

One car at that dealer. Its first photo makes it.

stockNumber: A1234

Photo

photoId: PMP-88231

frontAnglecompleted

Photo

photoId: PMP-88232

sidequality_assurance

Photo

photoId: PMP-88233

dashboardprocessing
+ the dealer's other vehicles
+ your other dealers
Each thing lives inside the one before. The label on the right of each is how a call names it.

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/:id until its status is completed or failed. Handy while you're building.
Your serverListwyse
  1. POST /photos

    the photo's link, the stock number, the dealer

  2. 202 Accepted, at once

    the photo, queued, and its credits held

  3. a few minutes

    We download it, make it, and put it through quality assurance

  4. POST to your webhook URL

    photo.completed, with the finished photo in it

  5. Reply 200 and save photo.url

    the link keeps working

The request is answered in a moment; the photo arrives later, on its own. Without a webhook, you ask for it instead (GET /photos/:id) until it's done.

A photo's life

A photo has one status at a time and only moves forward. completed and failed are the two ends.

queuedcredits heldprocessingquality_assurancecan add a few minutescompletedurl is readyfailedcredits given backcan't startcan't be madedoesn't passpassescredits kept
A photo's statuses. Its credits are held while it's on its way, kept when it's completed, and given back if it fails.
statusWhat's happeningIts credits
queuedReceived. We download it and work out what it shows.Held
processingBeing made.Held
quality_assuranceBeing checked against your original. This can add a few minutes.Held
completedDone: url is the finished file.Kept
failedCouldn'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 completed and sent to you.
  • It doesn't pass: the photo is failed, with error.code QUALITY_ASSURANCE_FAILED (“Did not pass quality assurance”). You aren't charged, and sending it again (the same photoId) 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:

WhenCredits
It's finished and sent backKept: that's the charge
It fails, or doesn't pass quality assuranceGiven back
It's made again during quality assuranceFree
It isn't a photo of a car, so comes back as it was (edited: false)Given back
You send the same photoId againFree: you get that photo back. A failed one is tried again, priced like a new photo
You ask for a redoCharged 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.

Upload a file
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 photoId twice 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.completed arrives 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 photoId you've sent before come back as they are (their ids are listed in reused), 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?

Send photos one at a time if your app uploads them as they're shot: each comes back as soon as it's ready. Use the vehicle call if you collect a car's photos first and want one signal for the whole car.

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

  1. Pick an https URL on your server and set it: PUT /webhook.
  2. Copy the secret from the answer (GET /webhook shows it again any time). It signs every event, so you can tell they're from us.
  3. Send yourself a test: POST /webhook/test. GET /webhook/deliveries then shows what your server answered.

What we send

A POST with a JSON body, and these headers:

HeaderWhat it holds
X-Listwyse-EventThe event's name.
X-Listwyse-DeliveryThe delivery's id (the same as id in the body).
X-Listwyse-SignatureThe signature: t=…,v1=… (below).
EventWhenIn data
photo.completedA photo is done.photo
photo.failedA photo couldn't be done.photo, with its error
vehicle.completedEvery photo from one vehicle call is done.vehicle, with those photos, and batchId
webhook.testYou asked for a test.a message
photo.completed
{
  "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.

Verify
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);
}
Use the body exactly as it arrived. Parsing the JSON and turning it back into text changes its bytes, and the signature won't match.

Answer fast, handle each event once

  • Reply with any 2xx within 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/deliveries shows every try, and POST /webhook/deliveries/:id/resend sends 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.

Wait for a photo
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:

  1. Listwyse's defaults (the Default column below).
  2. Your account's own settings, at the dealer id default: PATCH /dealers/default/settings. Every dealer follows them until it has its own.
  3. 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.

SettingChoicesDefaultWhat it does
background"keep", or a look's idkeepKeep the photo's own background, or put the car in a look.
outputFormatjpeg · png · webpjpegThe file type photos come back as.
cleanup0 As shot · 1 Detailed0 As shotHow clean the car looks: dirt, dust and water spots. Dents and scratches always stay.
backgroundTidy0 As shot · 1 Tidied · 2 Staged0 As shotHow much of the lot behind the car is tidied, when the background is kept.
reflections0 Minimal · 1 Natural0 MinimalHow much of the surroundings shows in the paint and glass.
framing0 Reframed · 1 As shot1 As shotWhether 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 idNameWhat it looks like
grey-studio-catalogueInfinitySeamless grey cyclorama.
panel-showroom-catalogueLumenGloss floor, light-panel ceiling.
showroom-set-v2SignatureLight-grey walls, a lit ceiling, a tiled floor.
outdoor-catalogueHorizonEmpty 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.

402
{
  "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…"
}
codeStatusWhat it meansWhat to do
INVALID_REQUEST400A field is missing or wrong.message names the field: fix it and send again.
INVALID_JSON400The body isn't valid JSON.Check how the body is built.
INVALID_BACKGROUND400No look with that id on your account.Use "keep" or an id from GET /templates.
DEALER_REQUIRED400You shoot for dealers and the call didn't name one.Send dealerCode or dealerId.
DUPLICATE_PHOTO_ID400One vehicle call has the same photoId twice.Give each photo its own photoId.
UNSUPPORTED_IMAGE400An uploaded file isn't a JPEG, PNG, WebP or GIF.Convert it, or send another file.
INVALID_WEBHOOK_URL400The webhook URL isn't https on the public internet.Use a public https URL.
ONE_DEALER_ACCOUNT400Adding a dealer to a dealership's own account.Ask us for an account that shoots for dealers.
UNAUTHORIZED401No key, or not a live one.Check the Authorization header.
OUT_OF_CREDITS402Your balance can't cover it. Nothing started.Top up, then send again.
NOT_FOUND, DEALER_NOT_FOUND404Not on your account.Check the id or code.
PHOTO_IN_PROGRESS409A redo or removal of a photo that's still being made.Wait until it's completed or failed.
PHOTO_ID_TAKEN409That photoId was already sent for another car.Use a new photoId for this car.
PHOTO_NOT_STORED409A redo of a photo we never received (its download failed).Send it again with POST /photos and the same photoId.
DEALER_CODE_TAKEN409Another of your dealers has that code.Pick another code.
NO_WEBHOOK_URL409A test event with no webhook URL set.Set one with PUT /webhook first.
DELIVERING409A resend of a delivery that's being sent right now.Wait a moment and look again.
TOO_LARGE413An uploaded file is over 4MB.Send it as a link instead.
INTERNAL_ERROR500Something went wrong on our side.Send it again; quote requestId if it keeps happening.
UNAVAILABLE503We 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.codeWhy
QUALITY_ASSURANCE_FAILEDDid not pass quality assurance, even after being made again on our side.
DOWNLOAD_FAILEDWe couldn't download the link: it isn't a public https link, isn't an image, or is over 12MB.
ANGLE_UNKNOWNWe couldn't tell what the photo shows. Send it again.
VEHICLE_FULLThe car already has 60 photos. Remove old ones to make room.
LOOK_UNAVAILABLEThe look can't be used any more (it was deleted, or isn't ready).
START_FAILEDIt couldn't be started; message says why.
GENERATION_FAILEDEvery image service we use failed on this photo.
START_TIMEOUTIt never got going. Send it again.
VEHICLE_GONEThe 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: message says 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 Code
claude mcp add --transport http listwyse https://listwyse.com/api/mcp \
  --header "Authorization: Bearer sk_live_YOUR_KEY"
Cursor · mcp.json
{
  "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.

id

string

Our id for the photo.

photoId

string or null

Your id for it, as you sent it.

vehicleId

string

Our id for its car.

stockNumber, vin

string or null

Its car's stock number and VIN.

dealerId

string or null

Our id for its dealer.

status

queued, processing, quality_assurance, completed or failed

Where it is. Only completed and failed are final.

statusText

string

A plain sentence for the status, ready to show people.

url

string or null

The finished file, once completed.

format

jpeg, png or webp

The file type of url.

edited

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.

angle

string or null

What the photo shows (frontAngle, interior…): as you sent it, or as we worked it out.

angleSource

given, detected or null

Whether you sent the angle or we worked it out.

background

string

"keep", or the look it was put in.

version

number

1, then 2 after a redo, and so on.

cost

number

Credits this version cost: the price while held or kept, 0 once given back.

note

string or null

Why something out of the ordinary happened, such as your original being sent back.

error

object or null

On a failed photo: code and message say why.

inputUrl

string or null

The link you sent (null for an uploaded file).

createdAt, updatedAt, completedAt

string

When it was sent, last changed, and finished (UTC, ISO 8601).

Example
The 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 vehicle

One car at one dealer. It's made the first time a photo arrives with its stock number (or VIN).

id

string

Our id for the car.

stockNumber, vin

string or null

How you named it. A VIN sent later is added to a car first sent by stock number.

dealerId

string or null

Our id for its dealer.

status

empty, processing or completed

processing while any of its photos is still going.

photoCount

number

How many photos it has.

counts

object

How many of its photos are at each status.

photos

list of photos

On GET /vehicles/:id (all of them) and POST /vehicles (that call's).

createdAt

string

When its first photo arrived.

Example
The 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"
    },
    "…"
  ]
}

The dealer

A dealership you shoot for.

id

string

Our id for the dealer: send it as dealerId.

name

string

Its name.

code

string or null

Your code for it: send it as dealerCode.

createdAt

string

When it was added.

Example
The dealer
{
  "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.

id

string

The delivery's id. A retry has the same id, so skip ids you've handled.

event

string

photo.completed, photo.failed, vehicle.completed or webhook.test.

createdAt

string

When the event happened.

data.photo

photo

On photo.completed and photo.failed.

data.vehicle, data.batchId

vehicle, string

On vehicle.completed: the car, with every photo from that vehicle call.

Example
The webhook event
{
  "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.

error.code

string

A stable code, such as OUT_OF_CREDITS.

error.message

string

What went wrong and, where it helps, what to do.

requestId

string

Quote it if you ask us about the call.

credits

object

On OUT_OF_CREDITS: balance, needed and pricePerPhoto.

Example
The error
{
  "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.

frontFront
rearRear
sideSide
frontAngleFront angle
rearAngleRear angle
wheelWheel close-up
engineEngine
trunkTrunk
signBadge or emblem
doorDoor close-up
headlightHeadlight close-up
taillightRear light close-up
interiorFrontFront row - head on
interiorSideBack row - side on
seatFront row - side on
dashboardDashboard
monitorScreen close-up
gearStickGear stick
steeringWheelSteering wheel
odometerOdometer / gauges
pedalsPedals
startButtonStart button
mirrorRearview mirror
columnControlsColumn controls
doorCardDoor card
seatControlsSeat controls
windowControlsWindow controls
keysKeys
bedTruck bed
sunroofRoof
thirdRowThird row
chargePortCharge port
hitchTow hitch
topDownTop down
frunkFront trunk
runningBoardsRunning boards
tailgateTailgate
rearScreensRear screens
damageDamage
exhaustExhaust
fuelDoorFuel door
spareSpare and tools
sideMirrorSide mirror
tireTreadTire tread
interiorAny other cabin photo
bedCoverTruck bed - roof on
bedTailgateOpenTruck bed - tailgate open
detailsAny other close-up
roofAboveRoof from above

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.

POST/photosUses credits

Send 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)

photoId
string

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.

url
https linkrequired

A public https link to the photo.

angle
angle

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.

stockNumber
string

The dealer's stock number. Photos with the same stock number (and dealer) are one vehicle.

vin
string

The 17-character VIN. Needed when there's no stockNumber.

dealerId
string

Our id for the dealer (GET /dealers). Needed if you shoot for dealers; a dealership's own account leaves it out.

dealerCode
string

Or your own code for the dealer, set on POST /dealers.

background
string

"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.

settings
object

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.

settings.cleanup
whole number

Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.

settings.backgroundTidy
whole number

Background tidy: how much of the lot behind the car is tidied, when the background is kept.

settings.reflections
whole number

Reflections: how much of the surroundings shows in the paint and glass.

settings.framing
whole number

Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.

output
object

The file you get back.

output.format
jpeg | png | webp

jpeg (the default), png or webp.

What happens

  1. We check the request and hold the photo's price from your credits. The answer comes back at once: the photo, queued.
  2. We find the car by its stock number (or VIN) at that dealer, or make it if this is its first photo.
  3. In the background: we download the photo, work out what it shows, and make it.
  4. 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.
  5. 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

DEALER_REQUIRED 400Your account shoots for dealers and the call didn't name one.
DEALER_NOT_FOUND 404No dealer with that dealerId or dealerCode on your account.
INVALID_REQUEST 400A field is missing or wrong (no stockNumber or vin, a bad VIN); message names it.
INVALID_BACKGROUND 400background isn't "keep" or a look on your account.
OUT_OF_CREDITS 402Your balance can't cover the photo. Nothing was started.
PHOTO_ID_TAKEN 409That photoId was already sent for a different car.
TOO_LARGE 413An uploaded file is over 4MB. Send it as a link instead.
UNSUPPORTED_IMAGE 400An uploaded file isn't a JPEG, PNG, WebP or GIF.
Request
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"
}'
Answer · 202
{
  "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
  }
}
GET/photos/:id

Check 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

:id
in the pathrequired

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

NOT_FOUND 404No photo with that id or photoId on your account.
Request
curl "https://listwyse.com/api/v1/studio/photos/PMP-88231" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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"
  }
}
GET/photos

List 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

vehicleId
string

Only this vehicle's photos.

dealerId
string

Only this dealer's photos.

status
queued | processing | quality_assurance | completed | failed

Only photos with this status.

limit
whole number

How many (1–100, default 50).

before
string

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.

Request
curl "https://listwyse.com/api/v1/studio/photos?status=processing&limit=20" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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"
}
POST/photos/:id/redoUses credits

Make 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

:id
in the pathrequired

Our photo id, or your photoId.

Body

background
string

"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.

settings
object

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.

settings.cleanup
whole number

Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.

settings.backgroundTidy
whole number

Background tidy: how much of the lot behind the car is tidied, when the background is kept.

settings.reflections
whole number

Reflections: how much of the surroundings shows in the paint and glass.

settings.framing
whole number

Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.

output
object

The file you get back.

output.format
jpeg | png | webp

jpeg (the default), png or webp.

What happens

  1. The price is held again: a redo is a new photo.
  2. The photo goes back to queued with version one higher, and runs like a new photo, its checks asked afresh.
  3. The earlier version's file keeps working at its link.

Errors

PHOTO_IN_PROGRESS 409The photo is still being made. Wait for it to finish.
PHOTO_NOT_STORED 409We never received the photo (its download failed). Send it again with POST /photos and the same photoId.
OUT_OF_CREDITS 402Your balance can't cover it.
INVALID_BACKGROUND 400background isn't "keep" or a look on your account.
NOT_FOUND 404No photo with that id or photoId on your account.
Request
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"
  }
}'
Answer · 202
{
  "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
  }
}
DELETE/photos/:id

Remove 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

:id
in the pathrequired

Our photo id, or your photoId.

Errors

PHOTO_IN_PROGRESS 409The photo is still being made.
NOT_FOUND 404No photo with that id or photoId on your account.
Request
curl -X DELETE "https://listwyse.com/api/v1/studio/photos/PMP-88231" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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.

POST/vehiclesUses credits

Send 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

stockNumber
string

The dealer's stock number. Photos with the same stock number (and dealer) are one vehicle.

vin
string

The 17-character VIN. Needed when there's no stockNumber.

dealerId
string

Our id for the dealer (GET /dealers). Needed if you shoot for dealers; a dealership's own account leaves it out.

dealerCode
string

Or your own code for the dealer, set on POST /dealers.

photos
list of objects, up to 60required

The car's photos, up to 60.

photos[].photoId
string

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.

photos[].url
https linkrequired

A public https link to the photo.

photos[].angle
angle

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.

background
string

"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.

settings
object

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.

settings.cleanup
whole number

Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay.

settings.backgroundTidy
whole number

Background tidy: how much of the lot behind the car is tidied, when the background is kept.

settings.reflections
whole number

Reflections: how much of the surroundings shows in the paint and glass.

settings.framing
whole number

Framing: whether a crooked photo may be straightened and re-cropped, or keeps its framing.

output
object

The file you get back.

output.format
jpeg | png | webp

jpeg (the default), png or webp.

What happens

  1. Every new photo's price is held together. If your balance can't cover them all, none start.
  2. The answer comes back at once: the vehicle, with this call's photos, queued.
  3. Each photo runs on its own and sends its own photo.completed or photo.failed webhook.
  4. 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

DEALER_REQUIRED 400Your account shoots for dealers and the call didn't name one.
DEALER_NOT_FOUND 404No dealer with that dealerId or dealerCode on your account.
INVALID_REQUEST 400A field is missing or wrong; message names it.
DUPLICATE_PHOTO_ID 400The same photoId appears twice in the call.
INVALID_BACKGROUND 400background isn't "keep" or a look on your account.
OUT_OF_CREDITS 402Your balance can't cover every new photo. Nothing was started.
PHOTO_ID_TAKEN 409One of the photoIds was already sent for a different car.
Request
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"
    }
  ]
}'
Answer · 202
{
  "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
  }
}
GET/vehicles/:id

Check 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

:id
in the pathrequired

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

NOT_FOUND 404No vehicle with that id on your account.
Request
curl "https://listwyse.com/api/v1/studio/vehicles/VEHICLE_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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"
      },
      "…"
    ]
  }
}
GET/vehicles

List 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

stockNumber
string

Find one vehicle by its stock number (with dealerId or dealerCode).

vin
string

Or by its VIN.

dealerId
string

—

dealerCode
string

—

limit
whole number

How many (1–100, default 50).

before
string

Paging: the createdAt of the last item you have; you get older ones.

Errors

DEALER_NOT_FOUND 404No dealer with that dealerId or dealerCode on your account.
Request
curl "https://listwyse.com/api/v1/studio/vehicles?stockNumber=A1234&dealerCode=RIV-014" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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.

GET/dealers

List dealers

Every dealer on your account, by name. An account that is itself one dealer lists none.

Request
curl "https://listwyse.com/api/v1/studio/dealers" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "dealers": [
    {
      "id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
      "name": "Riverton Acura",
      "code": "RIV-014",
      "createdAt": "2026-10-24T14:40:00.000Z"
    }
  ],
  "accountKind": "provider"
}
POST/dealers

Add 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

name
stringrequired

The dealer's name.

code
string

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

ONE_DEALER_ACCOUNT 400Your account is a single dealer, so it has no dealers to add.
DEALER_CODE_TAKEN 409Another of your dealers already has that code.
Request
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"
}'
Answer · 201
{
  "dealer": {
    "id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
    "name": "Riverton Acura",
    "code": "RIV-014",
    "createdAt": "2026-10-24T14:40:00.000Z"
  }
}
GET/dealers/:id

Get a dealer

Path

:id
in the pathrequired

Our dealer id, or your code.

Errors

DEALER_NOT_FOUND 404No dealer with that id or code on your account.
Request
curl "https://listwyse.com/api/v1/studio/dealers/RIV-014" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "dealer": {
    "id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
    "name": "Riverton Acura",
    "code": "RIV-014",
    "createdAt": "2026-10-24T14:40:00.000Z"
  }
}
PATCH/dealers/:id

Rename a dealer or change its code

Send only what changes. A code of null removes it.

Path

:id
in the pathrequired

Our dealer id, or your code.

Body

name
string

—

code
string or null

Your own id for the dealer; null removes it.

Errors

DEALER_CODE_TAKEN 409Another of your dealers already has that code.
DEALER_NOT_FOUND 404No dealer with that id or code on your account.
Request
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"
}'
Answer · 200
{
  "dealer": {
    "id": "d89df6f9-0c4e-4a43-b3d4-6a2f0c9e1b77",
    "name": "Riverton Acura",
    "code": "RIV-014",
    "createdAt": "2026-10-24T14:40:00.000Z"
  }
}
GET/dealers/:id/settings

Get 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

:id
in the pathrequired

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

DEALER_NOT_FOUND 404No dealer with that id or code on your account.
Request
curl "https://listwyse.com/api/v1/studio/dealers/RIV-014/settings" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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": [
          "…"
        ]
      }
    }
  }
}
PATCH/dealers/:id/settings

Change 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

:id
in the pathrequired

Our dealer id, your code, or default for your account's own settings.

Body

background
string

The background photos get when a call doesn't say: "keep" or a look id.

outputFormat
jpeg | png | webp

The file type photos come back as when a call doesn't say.

cleanup
whole number

Clean-up: how clean the car looks (dirt, dust, water spots). Dents and scratches always stay. A step number, as listed under options.

backgroundTidy
whole number

Background tidy: how much of the lot behind the car is tidied, when the background is kept. A step number.

reflections
whole number

Reflections: how much of the surroundings shows in the paint and glass. A step number.

framing
whole 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

INVALID_REQUEST 400A slider step that doesn't exist, or a field we don't know.
INVALID_BACKGROUND 400background isn't "keep" or a look on your account.
DEALER_NOT_FOUND 404No dealer with that id or code on your account.
Request
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"
}'
Answer · 200
{
  "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.

GET/webhook

Your webhook

The URL we send events to, the secret that signs them, and the events there are.

Request
curl "https://listwyse.com/api/v1/studio/webhook" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "webhook": {
    "url": "https://api.example.com/listwyse/events",
    "secret": "9f86d081884c7d65…",
    "events": [
      "photo.completed",
      "photo.failed",
      "vehicle.completed",
      "webhook.test"
    ]
  }
}
PUT/webhook

Set your webhook URL

An https URL on the public internet, or null to stop sending. The secret stays the same.

Body

url
https linkrequired

Your https endpoint, or null to stop webhooks.

Errors

INVALID_WEBHOOK_URL 400Not an https link on the public internet.
Request
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"
}'
Answer · 200
{
  "webhook": {
    "url": "https://api.example.com/listwyse/events",
    "secret": "9f86d081884c7d65…"
  }
}
POST/webhook/test

Send 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

NO_WEBHOOK_URL 409Set a URL first (PUT /webhook).
Request
curl -X POST "https://listwyse.com/api/v1/studio/webhook/test" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 202
{
  "delivery": {
    "id": "e2b1…",
    "event": "webhook.test",
    "status": "queued"
  }
}
GET/webhook/deliveries

Recent 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

limit
whole number

How many (1–100, default 50).

before
string

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).

Request
curl "https://listwyse.com/api/v1/studio/webhook/deliveries?limit=20" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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
}
POST/webhook/deliveries/:id/resend

Send a delivery again

Sends one delivery again now, with the same id and body.

Path

:id
in the pathrequired

The delivery's id.

Errors

NOT_FOUND 404No delivery with that id for your key.
DELIVERING 409It's being sent right now.
Request
curl -X POST "https://listwyse.com/api/v1/studio/webhook/deliveries/DELIVERY_ID/resend" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 202
{
  "delivery": {
    "id": "e2b1…",
    "status": "queued"
  }
}

Account and looks

Your key, your credits, and the looks a photo can be put in.

GET/account

Check 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.

Request
curl "https://listwyse.com/api/v1/studio/account" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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
  }
}
GET/templates

List 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.

Request
curl "https://listwyse.com/api/v1/studio/templates" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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.

GET/branding

The 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.

Request
curl "https://listwyse.com/api/v1/studio/branding" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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 }
    }
  }
}
POST/branding/designsUses credits

Paid 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.

Request
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"
  ]
}'
Answer · 202
{ "branding": { "assets": { "displayPlate": { "generating": true, "runId": "run_…" } } } }
POST/branding/photos

Use 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.

Request
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"
}'
Answer · 200
{ "kind": "floorMat", "url": "https://…", "branding": { … } }
PATCH/branding

Switch which saved design new vehicles get.

Request
curl -X PATCH "https://listwyse.com/api/v1/studio/branding" \
  -H "Authorization: Bearer $LISTWYSE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "chosen": {
    "floorMat": "https://…"
  }
}'
Answer · 200
{ "branding": { … } }
DELETE/branding/designs

Delete one saved design. If it was chosen, the next saved one is.

Request
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://…"
}'
Answer · 200
{ "branding": { … } }
POST/templates/custom

Start 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.

Request
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"
  ]
}'
Answer · 201
{
  "template": {
    "id": "d2343166-…",
    "templateId": "custom:d2343166-…",
    "name": "Our showroom",
    "status": "draft",
    "photoUrls": ["https://…"],
    "exampleUrls": [],
    "selectedUrl": null
  }
}
GET/templates/custom

Every 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.

Request
curl "https://listwyse.com/api/v1/studio/templates/custom" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{ "templates": [{ "id": "…", "templateId": "custom:…", "status": "ready", … }] }
GET/templates/custom/:id

One Build-your-own look. :id is its id or its templateId.

Path

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{ "template": { "status": "examples", "exampleUrls": ["https://…"], … } }
POST/templates/custom/:id/photos

Add more photos of the building: JSON { urls }, or a multipart file.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
  ]
}'
Answer · 200
{ "template": { "photoUrls": ["https://…", "https://…"], … } }
DELETE/templates/custom/:id/photos

Take a photo off. The look goes back to draft until built again.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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://…"
}'
Answer · 200
{ "template": { "status": "draft", … } }
POST/templates/custom/:id/buildUses credits

Paid 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

:id
in the pathrequired

The id from the call that made it.

Request
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
}'
Answer · 202
{
  "template": { "status": "generating", … },
  "runId": "run_…",
  "operationId": "…",
  "started": true
}
PATCH/templates/custom/:id

Pick the built photo (selectedUrl, one of exampleUrls) to make the look ready, or rename it / change its note.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{ "template": { "status": "ready", "selectedUrl": "https://…", … } }
POST/templates/custom/:id/plate

Skip the build: use a finished, empty photo of the dealership. The look is ready at once.

JSON { url, name? }, or multipart file + name.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{ "template": { "status": "ready", … } }
DELETE/templates/custom/:id

Delete a Build-your-own look. Vehicles already made with it keep their photos.

Path

:id
in the pathrequired

The id from the call that made it.

Request
curl -X DELETE "https://listwyse.com/api/v1/studio/templates/custom/TEMPLATE_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{ "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.

POST/sessions

Start 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).

Request
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"
}'
Answer · 200
{
  "session": {
    "id": "…",
    "status": "needs_photos",
    "label": "2024 Lexus GX 550",
    "externalId": "stock-4412",
    "vin": "JTJTBKDJ4R5012345"
  },
  "reused": false
}
GET/sessions

List 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.

Request
curl "https://listwyse.com/api/v1/studio/sessions" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "sessions": [
    {
      "id": "…",
      "label": "2024 Lexus GX 550",
      "externalId": "stock-4412",
      "uploadCount": 12,
      "imageCount": 4,
      "enhancedCount": 0,
      "showroomImageCount": 4,
      "videoCount": 0,
      "generating": true,
      "thumbnailUrl": "https://…"
    }
  ]
}
GET/sessions/:id

The 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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": []
  }
}
PATCH/sessions/:id

Update the display name and/or VIN.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{ "ok": true, "label": "GX 550 Overtrail", "vin": "JTJTBKDJ4R5012345" }
DELETE/sessions/:id

Delete the session and its results from your account.

Photo and result URLs you already copied keep working.

Path

:id
in the pathrequired

The id from the call that made it.

Request
curl -X DELETE "https://listwyse.com/api/v1/studio/sessions/SESSION_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{ "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.

POST/sessions/:id/photos

Add 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
    }
  ]
}'
Answer · 200
{
  "uploads": [
    { "url": "https://…", "name": "front.jpg" }
  ],
  "slots": { "front": ["https://…"], "side": ["https://…"] },
  "skipped": 0
}
DELETE/sessions/:id/photos

Take 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{
  "ok": true,
  "uploads": [{ "url": "https://…" }],
  "slots": { "front": ["https://…"] }
}
POST/sessions/:id/import

Pull 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 202
{
  "pending": true,
  "runId": "run_…",
  "listingUrl": "https://dealer.example/inventory/4412",
  "operationId": "…",
  "pollIntervalMs": 5000
}
GET/sessions/:id/import

Check one import: still running, the photos it added, or why it failed.

Add ?runId=run_….

Path

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/import" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "vehicleName": "2024 Lexus GX 550",
  "label": "2024 Lexus GX 550",
  "vin": "JTJTBKDJ4R5012345",
  "uploads": [{ "url": "https://…", "name": "listing-1.jpg" }],
  "skipped": 0,
  "runId": "run_…"
}
POST/sessions/:id/sort

Group 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

:id
in the pathrequired

The id from the call that made it.

Request
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
}'
Answer · 202
{
  "pending": true,
  "runId": "run_…",
  "operationId": "…"
}
GET/sessions/:id/sort

Check 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/sort" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "pending": true,
  "runId": "run_…"
}
POST/sessions/:id/branding

Build 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{
  "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).

POST/sessions/:id/vin

Set 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{
  "vin": "JTJTBKDJ4R5012345",
  "label": "2024 Lexus GX 550 Overtrail",
  "decoded": { "year": 2024, "make": "Lexus", "model": "GX 550", "trim": "Overtrail", "fuel": "Gasoline" }
}
GET/vin/:vin

Window-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
in the pathrequired

vin

Request
curl "https://listwyse.com/api/v1/studio/vin/1HGCV1F34PA012345" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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 }
  }
}
POST/sessions/:id/shot-plan

Smart 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

:id
in the pathrequired

The id from the call that made it.

Request
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
}'
Answer · 200
{
  "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"
}
GET/shot-plan

The same plan for a VIN before the car is photographed, so you know which photos to take. Nothing is saved.

Add ?vin=….

Request
curl "https://listwyse.com/api/v1/studio/shot-plan" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{ "plan": { "vin": "…", "shots": [ … ], "highlights": [ … ] } }
POST/sessions/:id/check-shot

Check 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{
  "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.

POST/sessions/:id/enhance

No 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
  ]
}'
Answer · 202
{
  "preparing": true,
  "runId": "run_…",
  "operationId": "…",
  "jobs": [],
  "pollIntervalMs": 2500
}
GET/sessions/:id/enhance

Wait 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/enhance" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "completed": true,
  "jobs": [
    { "presetId": 29001, "title": "Enhance — Front", "requestId": "…" }
  ]
}
POST/sessions/:id/images

Generate 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 202
{
  "preparing": true,
  "runId": "run_…",
  "templateId": "panel-showroom-catalogue",
  "jobs": [],
  "pollIntervalMs": 2500
}
GET/sessions/:id/images

Wait 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/images" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "templateId": "panel-showroom-catalogue",
  "jobs": [{ "presetId": 201, "title": "Hero", "requestId": "…" }],
  "brandingAssets": { "wallSign": "https://…" }
}
POST/sessions/:id/video

Start 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 202
{
  "preparing": true,
  "runId": "run_…",
  "templateId": "panel-showroom-catalogue",
  "pollIntervalMs": 2500
}
GET/sessions/:id/video

Wait 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/video" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "queued": true,
  "completed": false,
  "requestId": "…",
  "templateId": "panel-showroom-catalogue"
}
POST/sessions/:id/voiceover

Voice 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

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 202
{
  "pending": true,
  "runId": "run_…",
  "pollIntervalMs": 2500
}
GET/sessions/:id/voiceover

Wait 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

:id
in the pathrequired

The id from the call that made it.

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/voiceover" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "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.

POST/sessions/:id/versions

Make one of a shot's versions (images[].urls) its current photo.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
}'
Answer · 200
{ "ok": true, "presetId": 201, "url": "https://…/v1.jpg" }
DELETE/sessions/:id/versions

Delete generated photos. A shot with no versions left drops out of images.

Path

:id
in the pathrequired

The id from the call that made it.

Request
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"
    }
  ]
}'
Answer · 200
{ "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.

GET/sessions/:id/jobs/:requestId

Inspect one photo job, including the finished file or the error.

Job status uses succeeded / failed (not completed / error like session.images[]).

Path

:id
in the pathrequired

The id from the call that made it.

:requestId
in the pathrequired

requestId

Request
curl "https://listwyse.com/api/v1/studio/sessions/SESSION_ID/jobs/REQUEST_ID" \
  -H "Authorization: Bearer $LISTWYSE_KEY"
Answer · 200
{
  "job": {
    "requestId": "…",
    "operationId": "…",
    "kind": "image",
    "presetId": 201,
    "title": "Hero",
    "status": "succeeded",
    "result": { "images": [{ "url": "https://…" }] }
  }
}

Questions about the API? Write to your Listwyse contact with the call's requestId.