Limits & uploads
Upload constraints, rate limits, quotas, and the usage endpoint.
Upload constraints
| Constraint | Value |
|---|---|
| Formats | JPEG, PNG, WebP |
| Max size | 4 MB |
HEIC (the iPhone default) is not accepted — convert before upload. The widget handles format guidance for the customer automatically.
The limits
Every limit below is enforced at job creation: an over-limit request fails fast and no GPU work is ever queued, so a rejection costs you nothing.
| Limit | Default | Scope |
|---|---|---|
| Uploads per shopper | 10 | Rolling 24 hours |
| Uploads per key | 200 | Rolling 24 hours |
| Renders per shopper | 25 | Rolling 24 hours |
| Renders per key | 200 | Rolling 24 hours |
| Concurrent renders per key | 2 | In flight at once |
| Requests per IP | 60/minute | Across the pipeline POSTs |
The daily windows are rolling, not calendar days: capacity returns continuously as the oldest events pass the 24-hour mark, rather than all at once at midnight.
Every value is configurable per instance (set on the instance's API tab by your Viewa contact), and can be overridden per API key — so a server-side key and a browser key on the same instance can carry different ceilings. Changes take effect immediately, with no redeploy on your side. Contact the Viewa team to have a limit raised.
Cache hits are free
A render whose selections exactly match one you have already run is served from
the composition-hash cache. It never reaches the GPU, so it consumes no quota and
counts toward no cap — even for a shopper who is already at their daily limit.
It is still reported (as cachedRenders) so your usage numbers reconcile.
Without a visitor token
The per-shopper caps need a shopper to attribute the request to. If your instance does not require visitor tokens and your client sends none, every upload mints a new visitor, so the per-shopper upload cap has nothing to bite on: those callers are held to the per-key daily cap and the per-IP burst limit instead. Renders are unaffected — a render inherits its image's shopper either way.
When you hit one
Both refusals are 429 with problem+json — see
Errors.
code | What it means | What to do |
|---|---|---|
rate_limited | Transient: the per-IP burst, or your key's concurrent-render ceiling. | Wait the seconds in the Retry-After header, then retry. |
quota_exceeded | A daily allowance is used up. detail names which cap and its value. | Don't spin: capacity returns as the rolling window advances. Surface it to the shopper, or ask Viewa to raise the cap. |
Only rate_limited carries Retry-After. Treat a quota_exceeded as a state
to report, not a call to retry in a loop.
Checking your usage
GET /api/v1/usage reports what each of your keys consumed, for reconciling an
invoice. It is secret-key only — it enumerates every key on your instance,
which a browser never needs.
curl "https://your-instance.example/api/v1/usage?from=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sk_..."{
"period": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-09-14T09:12:44.301Z" },
"data": [
{
"key": { "label": "Storefront", "publishableKey": "pk_...", "status": "active" },
"uploads": 412,
"renders": 287,
"cachedRenders": 96
}
]
}from and to are RFC 3339 date-times with an offset (...Z or
...+10:00) — a bare date is rejected rather than guessed at — and the period is
half-open, [from, to), so consecutive months never double-count an event. Omit
them and you get the current UTC calendar month up to now. A period may span at
most 400 days.
renders counts render jobs dispatched to the pipeline (cache hits excluded,
listed under cachedRenders); a render that failed to dispatch is neither
billed nor counted. cachedRenders is shown alongside it so the totals
reconcile.
Rate limiting uses Upstash Redis. Leave the UPSTASH_REDIS_REST_* variables
unset in local development to disable IP burst limiting, or point them at a
dedicated dev database — never the production one.