API Error Reference
Every error code the Visual Search API can return, with cause and recommended fix.
Error envelope
All API errors return JSON with the same shape:
{
"error": "image_download_failed", // machine-readable code
"message": "Failed to download image: HTTP 404: Not Found", // human-readable
"details": { "url": "https://example.com/photo.jpg" }, // optional context
"request_id": "a1b2c3d4e5f6..." // quote this in support tickets
}
The request_id is also echoed in the X-Request-ID response
header. Rate-limit 429s additionally carry X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset, and
Retry-After.
Retries should be safe. Attach an
Idempotency-Key
header (any unique string up to 255 chars) on POST / PUT / PATCH / DELETE
requests. If the same key is replayed within 24 h the server returns the
original response instead of executing the operation again. Duplicate keys
with a different body return 409 idempotency_conflict.
Authentication & authorization
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
unauthorized |
401 | Missing or invalid Authorization: Bearer … header. |
Obtain a token via POST /oauth/token (client-credentials) or POST /oauth/login. Attach as Authorization: Bearer <token>. |
token_expired |
401 | Access token's exp claim has passed. |
Exchange the old token for a fresh one via POST /oauth/refresh (5-minute grace window). The old token is revoked atomically. |
token_revoked |
401 | Token was explicitly revoked via POST /oauth/revoke or rotated. |
Re-authenticate. The old token cannot be reused. |
forbidden |
403 | Token is valid but the caller lacks the required scope / permission. | Check the user's group permissions (GET /v1/security/permissions/me), or escalate to a client-owner token. |
Validation & input
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
validation_error |
422 | Request body failed Pydantic validation (wrong types, missing required field, out-of-range values). | Read details[] — each entry has loc (field path), type (error kind), and msg (human explanation). Fix the offending field and retry. |
not_found |
404 | The product / client / webhook / job referenced by path or body doesn't exist (or doesn't belong to the caller's tenant). | Verify the ID exists via the corresponding GET endpoint, or re-index. |
already_exists |
409 | A resource with the same primary key already exists (duplicate client email, duplicate product ID, etc.). | Use a different identifier, or use PUT to update the existing resource. |
invalid_url |
400 | The supplied URL resolves to a private / restricted network (SSRF protection). | Use a publicly routable URL. The server refuses RFC 1918, loopback, link-local, and metadata IP addresses. |
Image handling
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
image_download_failed |
400 | Couldn't download the URL you provided (404, DNS failure, timeout, TLS error, etc.). | Verify the URL responds with 200 and returns an image body. details.url echoes the URL we tried. |
unsupported_format |
415 | The downloaded bytes aren't a supported image format. | Use JPEG, PNG, or WebP. HTML, PDFs, SVGs, and HEIC are rejected. |
image_too_large |
413 | Image exceeds the maximum size limit. | Compress or resize the image below 10 MB before indexing. |
image_blocked_by_moderation |
403 | Image rejected by the tenant's content-moderation policy. Returned only when the moderation policy is in an enforce mode (moderate / strict) and the image scored above the category threshold. |
Inspect details.flagged_categories to see which categories tripped (e.g. nsfw_explicit, violence_gore). Fix the source asset, or relax the policy under /portal/moderation if it's a false positive. |
Rate limits & plan
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
rate_limit_exceeded |
429 | Too many requests for the caller's plan rate-limit window. | Honour the Retry-After header. Implement exponential back-off. Upgrade your plan for a higher ceiling. |
too_many_requests |
429 | Per-identity throttle on /oauth/login and /oauth/token tripped — too many failed attempts for this email or client_id (regardless of source IP). Resets after the lockout window or on a successful auth. |
Wait for details.retry_after seconds, then try again. If you've forgotten your password, use the password-reset flow instead of guessing. |
requires_2fa |
401 | Email + password were correct but the user has 2FA enabled and the request didn't include a totp_code. Returned only by /oauth/login. |
Prompt the user for their 6-digit authenticator code, then resend the same login request with totp_code populated. Don't retry without the code — it'll get the same response. |
invalid_2fa |
401 | The totp_code in the login body didn't verify against the user's TOTP secret. Returned only by /oauth/login. |
Authenticator codes refresh every 30 seconds — re-prompt and try again with a fresh code. Counts against the per-identity login throttle, so don't retry indefinitely. |
plan_limit_exceeded |
403 | Hit a hard quota (e.g. max products indexed for your plan). | Delete unused products or upgrade. details includes current and max_allowed. |
bulk_not_allowed |
403 | The current plan doesn't allow bulk indexing jobs. | Upgrade to a plan with bulk_allowed: true, or index products one at a time. |
Idempotency
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
idempotency_conflict |
409 | You reused an Idempotency-Key with a different request body / method / path. |
Use a new key for a new logical operation. Idempotency keys are scoped to the tenant and TTL'd for 24 h. |
idempotency_in_progress |
409 | A concurrent request with the same key is still executing. | Retry after a short delay (the middleware caches for 24 h once the first call finishes). |
idempotency_invalid_key |
400 | Key contains non-printable characters or exceeds 255 bytes. | Use a UUIDv4 or any opaque printable-ASCII string ≤ 255 chars. |
Server-side
| Code | HTTP | Cause | How to fix |
|---|---|---|---|
internal_error |
500 | Unexpected server error. Fully traced server-side. | Retry (it's an idempotent-safe state if you used Idempotency-Key). If it persists, email [email protected] with the request_id. |
model_loading |
503 | The CLIP model is still warming up after a cold start (rare). | Retry after a few seconds. Typically resolves within 30 s of deploy. |
service_unavailable |
503 | An upstream dependency (Qdrant, Stripe, AI proxy) is unreachable. | Retry with back-off. Monitor /v1/ai/health and the system status page for current state. |
Recommended request pattern
POST /v1/products/bulk Authorization: BearerIdempotency-Key: 7f0c4b2a-4e61-4d62-8a8f-9d30b1ff84aa Content-Type: application/json { "products": [ ... ] }
On network failure, retry with the same
Idempotency-Key. The server will return the original
response instead of indexing twice. On 5xx keep retrying
(those aren't cached). On 4xx fix the request and use a
new key.