viewa/docs
Core Concepts

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")
statusMeaning
preprocessingQueued or running — keep polling.
readyUsable for areas and renders.
failedTerminal. 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.

On this page