Photos & preprocessing
Upload a customer photo and track its preprocessing lifecycle.
A visualisation session starts with the customer's photo. Upload it in a single
request — the file goes in the POST body — and you get back an image resource
(img_…) that is already being preprocessed:
curl -X POST https://your-instance.example/api/v1/images \
-H "Authorization: Bearer pk_..." \
-F "file=@room.jpg"{
"id": "img_7bQ2mR9vXpL4dK1sZeCt",
"status": "preprocessing",
"width": 1024,
"height": 768,
"url": "https://your-instance.example/api/v1/images/img_7bQ2mR9vXpL4dK1sZeCt/file",
"error": null,
"visitor": {
"id": "vis_5nQ8xR2wKt7bZm3LpVdC",
"token": "vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…",
"expiresAt": "2026-12-01T00:00:00.000Z"
}
}Send the file either as a multipart/form-data part named file (what a browser
FormData produces) or as the raw request body — both work.
This is the only pipeline call that never requires a visitor token, and it is where
tokens come from: the response also carries a visitor block whose token you
should store and send as X-Viewa-Visitor-Token on every later call, this image's
own GET included. Whether later calls require it is a per-instance setting —
see Visitor tokens.
What the server accepts
JPEG, PNG, or WebP, up to 4 MB. Validation happens server-side against the
actual bytes, not your declared Content-Type — a mislabelled or undecodable
file is rejected with a problem+json error
(unsupported_media_type, 415) and an oversized one with file_too_large (413).
The photo is resized server-side before preprocessing, so the resource's width
and height are the working dimensions, not those of the file you sent. The stored
copy is normalised: the EXIF orientation is applied to the pixels and the bytes are
re-encoded as JPEG with the camera EXIF block kept, so you never need to pre-rotate
a phone photo.
The lifecycle
Preprocessing extracts what the later pipeline stages need from the photo (depth,
surface geometry, segmentation). It takes real time, so the resource is
asynchronous: poll it, and watch status move.
POST /api/v1/images → { "id": "img_…", "status": "preprocessing" }
GET /api/v1/images/{id} → poll until "ready" (or "failed")status | Meaning |
|---|---|
preprocessing | Queued or running — keep polling. |
ready | Usable for areas and renders. |
failed | Terminal. error says why; upload a new photo to retry. |
A failed image is resource state, not an HTTP error: the poll still answers
200, with a structured error object on the body —
{ "id": "img_…", "status": "failed", "error": { "code": "preprocessing_failed", "detail": "…" } }error.code is stable and safe to branch on: preprocessing_failed (the job
errored or could not be queued) or preprocessing_timeout (it did not finish in
time). Either way the resource is terminal — retry by uploading again.
problem+json is reserved for request-level failures (bad key, unknown id,
rejected file); see Errors.
The photo URL
url serves the stored photo through the API itself and takes no API key —
it works directly as an <img src>. The 20-character id is the credential; the
underlying storage host never appears in the contract. The bytes behind an id
never change (a new upload mints a new id), so the response is cacheable
indefinitely. url is null only if the photo bytes were never stored — an
image that failed before storage keeps a null url.
Idempotency
POST /v1/images honours an Idempotency-Key header, same as renders: a retried
upload with the same key returns the original image resource instead of
processing the photo twice. Key the attempt — generate a UUID when the customer
picks a photo, reuse it for retries of that same pick.