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:
| Code | Meaning | What to do |
|---|---|---|
visitor_token_required | The route needs a visitor token and none was sent | Upload a photo to be issued one |
visitor_token_invalid | Malformed, expired, or issued for a different instance | Discard 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.