viewa/docs
Core Concepts

Visitor tokens & privacy

How customer sessions and their data are handled.

Customers using the visualiser on your site are visitors. A visitor's uploads, areas and renders belong to them, and the API scopes every read and write to them with a signed visitor token.

Your API key says which shop is calling. The visitor token says which shopper.

Whether the token is mandatory is a per-instance setting — off by default, set on the instance's API tab by your Viewa contact on request. With it off, an API key alone can read any resource in your instance and protection is the unguessable img_/aset_/ren_ id, exactly as for the /file routes. With it on, every image, area and render call must present the token.

A token you do send always scopes the request to that shopper, either way — so the integration below is the recommended path regardless of the setting, and nothing changes when the gate is switched on.

Getting a token

Tokens are issued by the first upload. POST /api/v1/images sent without a visitor token mints a new visitor and returns it alongside the image:

{
  "id": "img_7bQ2mR9vXpL4dK1sZeCt",
  "status": "preprocessing",
  "visitor": {
    "id": "vis_5nQ8xR2wKt7bZm3LpVdC",
    "token": "vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…",
    "expiresAt": "2026-12-01T00:00:00.000Z"
  }
}

Persist visitor.token wherever you keep shopper state — session storage, a first-party cookie, your own session record. It is the only way back to that shopper's data.

Sending it

Every later pipeline call carries the token in its own header, next to the API key:

Authorization: Bearer pk_...
X-Viewa-Visitor-Token: vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…

That applies to reading an image, creating and reading areas, requesting and polling renders, and listing history. The two unauthenticated /file routes are the exception — an <img src> cannot send headers, so the opaque img_… / ren_… id is the credential there, exactly as before.

The header is on the CORS preflight allowlist, so browser-direct calls with a publishable key work unchanged.

Refresh and expiry

Send the token on later uploads too. The upload then attaches to the same visitor rather than starting a new one, and the response carries the visitor block again with a later expiresAt. The expiry slides forward on every upload, so an active shopper never ages out mid-session — always store the newest token you were given.

A token's lifetime matches the instance's retention window (90 days by default, set on the instance's API tab by your Viewa contact). Once it passes, the token stops verifying and the shopper starts over with a fresh upload.

When a token is refused

Both failures are 403 with a stable code. Only the first depends on the instance policy — visitor_token_required can happen only where the token is mandatory (an instance configured to require it, and the history endpoint, which always is). visitor_token_invalid applies everywhere, because a token that is sent is always verified:

CodeMeaningWhat to do
visitor_token_requiredThe route needs a visitor token and none was sentUpload a photo to be issued one
visitor_token_invalidMalformed, expired, or issued for a different instanceDiscard the stored token and upload again — a new visitor is minted

Treat visitor_token_invalid as "this shopper starts over": drop the token, start a fresh upload, and store the new one. Do not retry the same token.

A secret key does not bypass this. sk_ satisfies the API-key tier only; a server-side call still needs the shopper's token to read the shopper's data.

One shopper never sees another's

When a token is presented, a resource that exists but belongs to a different visitor answers the uniform 404 not_found — the same response an unknown id gets. Nothing distinguishes the two, so the API never confirms that someone else's id exists.

Areas and renders created without a token still inherit the shopper their source image belongs to, so a visitor's documents stay linked whether or not the client sent the header — which is what lets the deletion below treat them as one set.

Tokens are instance-bound too: a token issued under one instance is rejected under another, even for the same vis_… id.

History

With a valid token you can list the shopper's own renders to power a "your saved looks" experience:

curl https://your-instance.example/api/v1/visitors/me/renders \
  -H "Authorization: Bearer pk_..." \
  -H "X-Viewa-Visitor-Token: vis_..."

me is whoever the token names; there is no other addressable visitor. This endpoint requires the token on every instance, whatever the setting above — "the current visitor's renders" has no meaning without a visitor. The response is the standard paginated list envelope, newest first, and includes every state — generating, succeeded and failed. Each row is a full render resource, so it already carries the image and areaSet it came from, its result url, and the areas selections that produced it.

Deletion

Deleting a visitor is a single secret-key call. Use it to service a shopper's privacy request:

curl -X DELETE https://your-instance.example/api/v1/visitors/vis_5nQ8xR2wKt7bZm3LpVdC \
  -H "Authorization: Bearer sk_..."
{
  "visitor": "vis_5nQ8xR2wKt7bZm3LpVdC",
  "deleted": { "images": 2, "areaSets": 1, "renders": 2, "objects": 3 },
  "usageRecordsDetached": 3
}

It takes the visitor's id, not their token — your server is acting on the shopper's behalf, and a shopper exercising deletion has usually lost the token already. There is no visitor-token requirement on this route, and a publishable key is refused with key_type_mismatch.

What it removes: the visitor's uploaded photos, their areas, their renders, and the stored files behind all of them. Afterwards their history is empty and their result URLs stop resolving. objects counts the stored files, and is lower than images + renders whenever a render was served from the cache — a cached render shares the stored file of the one it was deduplicated from. Candidate keys derived from the deleted records are removed as well; if stored files cannot be removed after the records are deleted, the response is a 500 and the affected keys are written to the server log.

What it keeps: your metered usage counts, with the visitor cleared off them. Those rows are what your invoice is reconciled against, so they survive the deletion without referencing the shopper.

It is idempotent. Calling it again — or for a visitor id your instance never saw — answers 200 with every count zero, never a 404. Retry it freely.

Two residuals to know about

Derived files on the rendering host. Preprocessing produces depth, normal, segmentation and mask data on the machine that generates your composites. Those files are outside this endpoint and age out on that host's own schedule, within 90 days of being written.

The token is not revoked. It is stateless — there is no revocation list, so it keeps verifying until its own expiry. Discard it in your storefront when you delete the visitor. If you do not, an upload that still presents it will simply create new data under the same vis_ id.

Still to come. The scheduled retention purge — which will age this same data out automatically at the end of the instance's retention window — has not shipped yet. Until it does, deletion is on request only: a token passing its expiry stops the credential working, it does not remove anything.

On this page