Errors
Every FluidCloud API error is an HTTP status code paired with a JSON body. The body always has a top-level detail key — either a plain string or a structured object — so you can branch on the status code and, where present, the structured detail.error value.
This page is the catalog of statuses a partner key sees, the shapes of the detail body, and how both SDKs map each status to a typed error class.
- Base URL:
https://api-cloud.fluidvip.com/api/v1 - See also: authentication.md · rate-limits.md · reference/index.md
The error body
Every non-2xx response has this envelope:
{
"detail": "..."
}detail is one of two shapes:
| Shape | When | Example |
|---|---|---|
| String | Most 400/401/404/409/413/422 cases | {"detail": "file not found"} |
| Object | Cases that carry machine-readable context | {"detail": {"error": "insufficient_scope", "required": ["files:write"], "missing": ["files:write"]}} |
Always read the HTTP status first. Only inspect the inner detail.error field for the structured cases listed below (402 quota_exceeded, 403 insufficient_scope, 403 feature_not_in_plan). For everything else, treat detail as a human-readable message — do not pattern-match on its exact text, which may change.
Status catalog
| Status | Meaning | detail shape | What to do |
|---|---|---|---|
| 400 | Bad input — a malformed or missing field the API could parse but not accept | string | Fix the request and retry. |
| 401 | Missing, invalid, or revoked API key | string | Check the X-API-Key header. See authentication.md. |
| 402 | quota_exceeded — over your storage pool or share-link cap | object | Free space / links, or check reference/quota.md. |
| 403 | insufficient_scope (key lacks a scope) or feature_not_in_plan (subscription has no API access) | object | See Structured 403 bodies. |
| 404 | Not found — or not yours (a resource in another tenant) | string | The id does not exist for your tenant. |
| 409 | The file is not ready — sharing or serving a file that is not yet scanned clean | string | Poll until scan_status is clean, then retry. |
| 413 | Payload too large | string | Reduce the size or split the upload. |
| 422 | Validation error — the body is well-formed JSON but a field failed validation | string or object | Correct the indicated field and retry. |
| 429 | Rate limited | string | Honor the Retry-After header, then retry. See rate-limits.md. |
Notes on individual statuses
- 401 vs 403.
401means we could not authenticate you (no key, a malformed key, or a revoked one).403means we authenticated you, but you are not allowed to do this (a missing scope, or no API access in your plan). - 404, never 403, across tenants. FluidCloud never reveals whether an id exists in another tenant. A space, folder, file, or share that belongs to someone else returns
404exactly as if it did not exist. Do not interpret a404as proof a resource was deleted — it may simply not be yours. - 409 (quarantine-until-clean). Every file is automatically scanned on upload. A file can be shared or served only once
scan_statusisclean. Calling a share or serve operation on a file that is still scanning returns409. Poll the file untilscan_statusisclean, then retry. See guides/uploading.md and guides/raw-links.md. - Some actions require the dashboard, not an API key. A small number of owner-only, interactive actions are not exposed to API keys and cannot be performed with a partner key — perform those from the FluidCloud dashboard (
https://cloud.fluidvip.com).
Structured detail bodies
These three cases return an object detail with a stable error field you can branch on.
402 — quota_exceeded
You are over a plan limit: either the storage pool is full, or you have hit your share-link count cap. Creating share links and starting uploads are the operations that can raise this.
{
"detail": {
"error": "quota_exceeded"
}
}Read your current usage and limits from the quota endpoint to see which cap you hit (a limit of -1 means unlimited). See guides/quota-and-usage.md and reference/quota.md.
Structured 403 bodies
insufficient_scope
The key is valid, but it does not carry a scope the endpoint requires. A partner key holds a fixed scope set: spaces:read, spaces:write, folders:read, folders:write, files:read, files:write, shares:read, shares:write, quota:read. An endpoint outside that set returns this error. Scopes are checked with AND semantics — a request needs every scope its endpoint requires.
{
"detail": {
"error": "insufficient_scope",
"required": ["files:write"],
"missing": ["files:write"]
}
}required— all scopes the endpoint asked for.missing— the subset your key did not satisfy.
This is a configuration mismatch, not a transient error — retrying will not help. Check that you are calling an endpoint your scopes authorize. See authentication.md for the full scope-to-endpoint map.
feature_not_in_plan
Your subscription does not include API access. The FluidCloud API is included with the top (Elite) tier; on a plan without it, every API call returns this.
{
"detail": {
"error": "feature_not_in_plan"
}
}Upgrade your subscription in the dashboard (https://cloud.fluidvip.com → Settings → Developer) to enable API access, then issue a key. Keys cannot be created via the API.
SDK error-class mapping
Both SDKs parse the response and raise a typed error. Every error derives from a single base (FluidCloudError in Python, FluidCloudError in TypeScript), so you can catch all SDK/API failures in one place or branch on a specific subclass. Each typed error exposes the HTTP status and the parsed detail.
| HTTP status | Python class | TypeScript class | Covers |
|---|---|---|---|
| 401 | AuthError | AuthError | Missing / invalid / revoked key |
| 402 | QuotaExceededError | QuotaExceededError | quota_exceeded (storage or share-link cap) |
| 403 | PermissionError | PermissionError | insufficient_scope, feature_not_in_plan |
| 404 | NotFoundError | NotFoundError | Not found / not yours |
| 409 | ConflictError | ConflictError | File not yet scanned clean |
| 400, 413, 422, 429, and any other non-2xx | ApiError | ApiError | Everything else (read .status) |
The base
ApiErroris the catch-all. Statuses without a dedicated subclass — including400,413,422, and429— surface asApiError; branch onerror.statusto tell them apart. In Python, the 403 class is imported asPermissionError(it is defined internally asPermissionError_to avoid shadowing the builtin, but exported under thePermissionErrorname).
Python — try / except
from fluidcloud import FluidCloud
from fluidcloud.errors import (
FluidCloudError,
AuthError,
QuotaExceededError,
PermissionError,
NotFoundError,
ConflictError,
ApiError,
)
fc = FluidCloud(api_key="fck_live_...")
try:
share = fc.shares.create(file_id=file_id)
except AuthError:
# 401 — key missing / invalid / revoked
raise
except QuotaExceededError:
# 402 — over storage or share-link cap; detail.error == "quota_exceeded"
...
except PermissionError as e:
# 403 — inspect the structured detail
err = e.detail.get("error") if isinstance(e.detail, dict) else None
if err == "insufficient_scope":
... # key lacks a required scope; see e.detail["missing"]
elif err == "feature_not_in_plan":
... # subscription has no API access
except NotFoundError:
# 404 — the file id does not exist for your tenant
...
except ConflictError:
# 409 — file not scanned clean yet; poll scan_status, then retry
...
except ApiError as e:
# 400 / 413 / 422 / 429 / other — branch on e.status
if e.status == 429:
... # honor the Retry-After header (see rate-limits.md)
else:
...
except FluidCloudError:
# any other SDK-level failure
raiseTypeScript — try / catch
import { FluidCloud } from "fluidcloud";
import {
FluidCloudError,
AuthError,
QuotaExceededError,
PermissionError,
NotFoundError,
ConflictError,
ApiError,
} from "fluidcloud";
const fc = new FluidCloud({ apiKey: "fck_live_..." });
try {
const share = await fc.shares.create({ fileId });
} catch (e) {
if (e instanceof AuthError) {
// 401 — key missing / invalid / revoked
} else if (e instanceof QuotaExceededError) {
// 402 — over storage or share-link cap; detail.error === "quota_exceeded"
} else if (e instanceof PermissionError) {
// 403 — inspect the structured detail
const detail = e.detail as { error?: string; missing?: string[] };
if (detail?.error === "insufficient_scope") {
// key lacks a required scope; see detail.missing
} else if (detail?.error === "feature_not_in_plan") {
// subscription has no API access
}
} else if (e instanceof NotFoundError) {
// 404 — the file id does not exist for your tenant
} else if (e instanceof ConflictError) {
// 409 — file not scanned clean yet; poll scan_status, then retry
} else if (e instanceof ApiError) {
// 400 / 413 / 422 / 429 / other — branch on e.status
if (e.status === 429) {
// honor the Retry-After header (see rate-limits.md)
}
} else if (e instanceof FluidCloudError) {
// any other SDK-level failure
throw e;
} else {
throw e;
}
}When you make raw HTTP calls instead of using an SDK, replicate the same logic: switch on the HTTP status, and for 402/403 read detail.error for the structured cases above.
Raw HTTP — inspecting the body with curl
curl -i -X POST "https://api-cloud.fluidvip.com/api/v1/shares" \
-H "X-API-Key: fck_live_..." \
-H "Content-Type: application/json" \
-d '{"file_id": "00000000-0000-0000-0000-000000000000"}'A scope failure responds:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"detail":{"error":"insufficient_scope","required":["shares:write"],"missing":["shares:write"]}}Retrying
- Retry
429(after theRetry-Afterdelay) and transient5xxresponses with backoff. - Retry after a fix
409once the file'sscan_statusisclean. - Do not retry
400,401,403,404,413, and422unchanged — they signal a request or configuration problem that retrying will not resolve. Fix the input, key, scope, id, or plan first.
See rate-limits.md for per-key and per-action limits and the Retry-After contract.