Skip to content

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.


The error body

Every non-2xx response has this envelope:

json
{
  "detail": "..."
}

detail is one of two shapes:

ShapeWhenExample
StringMost 400/401/404/409/413/422 cases{"detail": "file not found"}
ObjectCases 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

StatusMeaningdetail shapeWhat to do
400Bad input — a malformed or missing field the API could parse but not acceptstringFix the request and retry.
401Missing, invalid, or revoked API keystringCheck the X-API-Key header. See authentication.md.
402quota_exceeded — over your storage pool or share-link capobjectFree space / links, or check reference/quota.md.
403insufficient_scope (key lacks a scope) or feature_not_in_plan (subscription has no API access)objectSee Structured 403 bodies.
404Not found — or not yours (a resource in another tenant)stringThe id does not exist for your tenant.
409The file is not ready — sharing or serving a file that is not yet scanned cleanstringPoll until scan_status is clean, then retry.
413Payload too largestringReduce the size or split the upload.
422Validation error — the body is well-formed JSON but a field failed validationstring or objectCorrect the indicated field and retry.
429Rate limitedstringHonor the Retry-After header, then retry. See rate-limits.md.

Notes on individual statuses

  • 401 vs 403. 401 means we could not authenticate you (no key, a malformed key, or a revoked one). 403 means 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 404 exactly as if it did not exist. Do not interpret a 404 as 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_status is clean. Calling a share or serve operation on a file that is still scanning returns 409. Poll the file until scan_status is clean, 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.

json
{
  "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.

json
{
  "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.

json
{
  "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 statusPython classTypeScript classCovers
401AuthErrorAuthErrorMissing / invalid / revoked key
402QuotaExceededErrorQuotaExceededErrorquota_exceeded (storage or share-link cap)
403PermissionErrorPermissionErrorinsufficient_scope, feature_not_in_plan
404NotFoundErrorNotFoundErrorNot found / not yours
409ConflictErrorConflictErrorFile not yet scanned clean
400, 413, 422, 429, and any other non-2xxApiErrorApiErrorEverything else (read .status)

The base ApiError is the catch-all. Statuses without a dedicated subclass — including 400, 413, 422, and 429 — surface as ApiError; branch on error.status to tell them apart. In Python, the 403 class is imported as PermissionError (it is defined internally as PermissionError_ to avoid shadowing the builtin, but exported under the PermissionError name).

Python — try / except

python
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
    raise

TypeScript — try / catch

ts
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

bash
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
HTTP/1.1 403 Forbidden
Content-Type: application/json

{"detail":{"error":"insufficient_scope","required":["shares:write"],"missing":["shares:write"]}}

Retrying

  • Retry 429 (after the Retry-After delay) and transient 5xx responses with backoff.
  • Retry after a fix 409 once the file's scan_status is clean.
  • Do not retry 400, 401, 403, 404, 413, and 422 unchanged — 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.

FluidCloud API — part of the Fluidvip ecosystem.