Error Reference
All Picora API errors return a consistent JSON envelope with both a human message and a machine-readable code.
Response shape
{ "success": false, "error": "Human-readable description", "code": "MACHINE_CODE", "meta": { ...endpoint-specific context... }}The HTTP status code indicates the category; code identifies the specific error; meta provides structured context (e.g., meta.type for QUOTA_EXCEEDED, meta.existingId for DOC_HASH_DUPLICATE).
Every response also includes an x-request-id header — keep it when reporting issues to support; it lets us correlate logs across services.
HTTP status overview
| Status | Meaning |
|---|---|
400 | Bad Request — malformed request syntax |
401 | Unauthorized — missing or invalid auth |
403 | Forbidden — authenticated but not permitted |
404 | Not Found — resource doesn’t exist |
409 | Conflict — resource state collision |
413 | Payload Too Large — body exceeds limit |
422 | Unprocessable Entity — validation failed |
429 | Too Many Requests — rate limit |
451 | Unavailable for Legal / Quota Reasons — used for video bandwidth suspended and CN content moderation block |
500 | Internal Server Error |
502 | Bad Gateway — upstream (object storage / Bunny / Aliyun) error |
Authentication errors
| Code | Status | Description | How to fix |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing Authorization header, malformed API key, or expired JWT. | Include Authorization: Bearer sk_live_... (API Key) or refresh JWT via /v1/auth/refresh. |
API_KEY_REVOKED | 401 | API Key was manually revoked. | Create a new key in Settings → API Keys. |
TOKEN_EXPIRED | 401 | JWT access token (15-min lifetime) expired. | Refresh via /v1/auth/refresh. |
OAUTH_BEARER_INVALID | 401 | OAuth bearer token signature invalid or token revoked (v0.14+ MCP HTTP). | Re-authorize the AI client in Authorized apps. |
OAUTH_REFRESH_REPLAYED | 401 | A previously-rotated refresh token was replayed (v0.14+; auto-revokes the entire token chain as a security measure). | Re-authorize from scratch. |
Permission errors
| Code | Status | Description | How to fix |
|---|---|---|---|
FORBIDDEN | 403 | No permission for this resource (e.g., another user’s image, admin route without admin role). | Only access your own resources; for /v1/admin/* endpoints you need is_admin = 1. |
OAUTH_SCOPE_INSUFFICIENT | 403 | OAuth token lacks required scope (e.g., calling delete_doc with only docs:read). | Re-authorize with broader scopes. |
QUOTA_EXCEEDED | 403 | A quota dimension is exhausted. meta.type indicates which: img_storage / media_storage / doc_count / etc.; meta.used / meta.limit show numbers. | Delete unused resources or upgrade plan. |
PLAN_REQUIRED | 403 | Feature requires a higher tier (e.g., video upload requires pro). | Upgrade plan. |
COMPLIANCE_BLOCKED | 451 | (CN platform only) Aliyun content safety API flagged the upload as policy-violating. | See the included meta.reasons and submit appeal if you believe it’s a false positive. |
Resource errors
| Code | Status | Description | How to fix |
|---|---|---|---|
NOT_FOUND | 404 | Resource doesn’t exist. meta.entity indicates type (Image / Video / Document / CdnWhitelistEntry / etc.). | Verify the ID; the resource may have been deleted. |
CONFLICT | 409 | Conflicting state — e.g., email already registered. | Use a different value or resolve the conflict. |
DOC_HASH_DUPLICATE | 409 | Markdown content (rewritten + hashed) already exists for this user; meta.existingId returns the original document ID. | This is semantic — the duplicate is treated as a no-op; use the existing ID. Does not consume doc_count quota. |
Validation errors
| Code | Status | Description | How to fix |
|---|---|---|---|
VALIDATION_ERROR | 422 | Generic Zod schema validation failure; meta.path and meta.issues describe specifics. | Fix the indicated fields. |
INVALID_FILE_TYPE | 422 | Uploaded file format not supported. | Use accepted formats (see images / videos / audio). |
FILE_TOO_LARGE / 413 | 413 / 422 | File exceeds plan / endpoint limit. | Compress / split / upgrade. |
DOC_FILE_TOO_LARGE | 422 | Markdown content exceeds plan’s doc_max_file_bytes. | Split into multiple docs or upgrade. |
DOC_IMAGE_LIMIT_EXCEEDED | 422 | Embedded image count in Markdown exceeds plan’s doc_max_images_per_doc. | Reduce embedded images or upgrade. |
DOC_INVALID_PATTERN | 422 | (Admin CDN allowlist) Pattern contains protocol / path / illegal chars. | Use bare hostname, e.g., media.example.com (exact) or .example.com (suffix). |
Rate limit errors
| Code | Status | Description | How to fix |
|---|---|---|---|
RATE_LIMITED | 429 | Tier rate limit exceeded. Retry-After response header indicates seconds until reset. | Wait and retry; consider request batching or backoff. |
OAUTH_CLIENT_RATE_LIMITED | 429 | OAuth dynamic client registration rate limit hit (v0.14+). | Reduce registration frequency. |
Server errors
| Code | Status | Description | How to fix |
|---|---|---|---|
INTERNAL | 500 | Unexpected error; report with x-request-id. | Retry with exponential backoff; if persistent, contact support with the request ID. |
STORAGE_ERROR | 502 | Object storage (R2 / OSS / COS) PUT/GET failed after retries. | Retry; if persistent, see status page. |
DEPENDENCY_UNAVAILABLE | 502 | Upstream provider (Bunny / Aliyun VOD / Lemon Squeezy / etc.) timeout or 5xx. | Retry; if persistent, check status page. |
Bandwidth-specific status
Video playback respects monthly bandwidth quota with three states:
| State | Behavior | API code on playback |
|---|---|---|
ok | Full quality served | (no error) |
degraded | Only 360p variants in HLS playlist | (no error; status visible in metadata) |
suspended | All playback returns 451 | BANDWIDTH_SUSPENDED (in 451 body) |
The API itself (e.g., GET /v1/videos/{id}) is not rate-limited by bandwidth state — only the actual video.picora.me playback edge enforces it. See bandwidth & degradation.
Handling errors in code
const response = await fetch('https://api.picora.me/v1/images', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_KEY' }, body: formData,});
const json = await response.json();
if (!response.ok) { // Use json.code for programmatic decisions, json.error for display switch (json.code) { case 'QUOTA_EXCEEDED': console.error(`Quota exhausted: ${json.meta?.type}`); // prompt user to delete or upgrade break; case 'RATE_LIMITED': const retryAfter = response.headers.get('Retry-After'); console.warn(`Rate limited; retry in ${retryAfter}s`); break; case 'PLAN_REQUIRED': // route user to upgrade flow break; default: console.error(`Upload failed (${response.status} ${json.code}): ${json.error}`); } return;}
console.log('Uploaded:', json.data.url);For long-running scripts, also handle 429 retries with respect for the Retry-After header; many APIs silently rate-limit clients that ignore it.
Reporting bugs
When opening a support ticket, include:
- The
x-request-idfrom the failing response - Approximate timestamp
- Full request URL and method (sanitize any API Keys)
- Browser / OS / SDK version
Without x-request-id, locating logs is much harder.