Python SDK
The fluidcloud package is the official Python client for the FluidCloud Partner API. It wraps every endpoint your API key is authorized to call, hides the multi-step upload flow behind a single files.upload(...), and maps HTTP errors to typed exceptions.
This page is the method-by-method reference. For end-to-end task walkthroughs see Uploading, Raw links, Organizing files, and Quota & usage. For the underlying HTTP shapes, see the API reference.
Install
pip install fluidcloudThe client requires Python 3.8+ and pulls in httpx as its only runtime dependency.
Construct a client
from fluidcloud import FluidCloud
fc = FluidCloud(api_key="fck_live_...")Create your key in the FluidCloud dashboard at cloud.fluidvip.com under Settings → Developer → API keys (an active subscription is required). Keys cannot be created via the API. See Authentication for details.
Constructor parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | — (required) | Your partner key (fck_live_... or fck_test_...). Sent automatically as the X-API-Key header. |
base_url | str | https://api-cloud.fluidvip.com | API origin. The default is production; you should not need to change it. |
timeout | float | 300.0 | Per-request timeout in seconds. Uploads can be slow, so the default is generous. |
fc = FluidCloud(
api_key="fck_live_...",
base_url="https://api-cloud.fluidvip.com",
timeout=300.0,
)All requests target the versioned API under /api/v1, so the effective endpoint base is https://api-cloud.fluidvip.com/api/v1. You pass the bare origin; the client appends the /api/v1 prefix for you.
Lifecycle
The client opens a pooled HTTP connection. Close it when you are done, or use it as a context manager so it closes automatically:
with FluidCloud(api_key="fck_live_...") as fc:
spaces = fc.spaces.list()
# connection closed here
# or, explicitly:
fc = FluidCloud(api_key="fck_live_...")
try:
...
finally:
fc.close()Resources
A constructed client exposes five resource namespaces. The methods you have access to are listed below; each shows its signature, the HTTP endpoint it calls, and what it returns. The return types are light dataclasses (see Result objects).
fc.spaces
A Space is the top-level container for your files and folders. Requires spaces:read / spaces:write.
| Method | Endpoint | Returns |
|---|---|---|
fc.spaces.list() | GET /spaces | list[Space] |
fc.spaces.create(name) | POST /spaces | Space |
spaces = fc.spaces.list()
brand = fc.spaces.create("Brand Assets")
print(brand.id) # UUID stringSee Spaces reference.
fc.folders
Folders organize files within a Space. delete is a soft-delete (the folder goes to trash); restore brings it back. Requires folders:read / folders:write.
| Method | Endpoint | Returns |
|---|---|---|
fc.folders.list(space_id, parent_id=None, *, trash=False) | GET /folders | list[Folder] |
fc.folders.create(name, space_id, parent_id=None) | POST /folders | Folder |
fc.folders.rename(folder_id, name) | PATCH /folders/{folder_id} | Folder |
fc.folders.move(folder_id, parent_id) | PATCH /folders/{folder_id} | Folder |
fc.folders.delete(folder_id) | DELETE /folders/{folder_id} | Folder |
fc.folders.restore(folder_id) | POST /folders/{folder_id}/restore | Folder |
inbox = fc.folders.create("inbox", space_id=brand.id)
# List the Space root (default). Pass parent_id to list inside a folder,
# or trash=True to list soft-deleted folders.
top = fc.folders.list(brand.id)
children = fc.folders.list(brand.id, parent_id=inbox.id)
trashed = fc.folders.list(brand.id, trash=True)
fc.folders.rename(inbox.id, "incoming")
fc.folders.move(inbox.id, parent_id=None) # None = move to Space root
fc.folders.delete(inbox.id) # soft-delete to trash
fc.folders.restore(inbox.id)Note:
movesendsparent_idexplicitly, so passingNonemeans "move to the Space root" — it is not the same as omitting the argument.
See Folders reference and Organizing files.
fc.files
The core resource: upload, list, fetch, organize, soft-delete, and mint links. Requires files:read / files:write (and shares:write for public_url / signed_url, which mint share links). Every uploaded file is automatically virus-scanned; a file must reach scan_status == "clean" before it can be shared or served, otherwise link minting returns 409 (quarantine-until-clean).
| Method | Endpoint(s) | Returns |
|---|---|---|
fc.files.upload(file, space_id, *, folder_id=None, name=None, content_type=None, public=False, client_key=None) | POST /uploads/initiate → presigned PUT → POST /uploads/complete | File |
fc.files.list(space_id, folder_id=None, *, trash=False) | GET /files | list[File] |
fc.files.get(file_id) | GET /files/{file_id} | File |
fc.files.resolve(client_key) | GET /files/resolve | File |
fc.files.rename(file_id, name) | PATCH /files/{file_id} | File |
fc.files.move(file_id, folder_id) | PATCH /files/{file_id} | File |
fc.files.delete(file_id) | DELETE /files/{file_id} | File |
fc.files.restore(file_id) | POST /files/{file_id}/restore | File |
fc.files.download_url(file_id) | GET /files/{file_id}/download | str |
fc.files.public_url(file_id) | POST /shares | Link |
fc.files.signed_url(file_id, *, expires_in_days=7, permission="view") | POST /shares | Link |
upload
upload accepts a filesystem path, raw bytes, or an open binary file object, and hides the entire presign → direct-PUT → complete flow (including multipart for large files). Your bytes go directly to storage via a presigned URL — the API never proxies them.
# From a path — name and content type are inferred from the filename.
asset = fc.files.upload("logo.png", space_id=brand.id)
# From bytes — supply a name so the type can be guessed.
asset = fc.files.upload(b"...", space_id=brand.id, name="report.pdf")
# Into a folder, with an explicit public hotlink minted on the result.
asset = fc.files.upload(
"out.png",
space_id=brand.id,
folder_id=inbox.id,
public=True,
)
print(asset.public_url) # permanent hotlink on https://files.fluidadmin.com/s/<token>Parameters:
file— a path (str),bytes/bytearray, or an open binary file object.space_id— the Space to store the file in (required).folder_id— optional folder within the Space; defaults to the Space root.name— override the stored filename; defaults to the source filename (orupload.binfor raw bytes).content_type— override the MIME type; defaults to a guess from the name.public— whenTrue, also mint a permanent public hotlink and set it onresult.public_url.client_key— your own logical key for this file (e.g.results/job-7/out.png). Store it instead of FluidCloud'sid, then fetch the file later withfiles.resolve(client_key)— no key→id map needed on your side. Seeresolvebelow.
See Uploading and the Uploads reference for the raw three-step flow if you ever bypass the SDK.
resolve
Fetch a file by the client_key you supplied at upload time, scoped to your tenant. Raises NotFoundError if there is no live match.
fc.files.upload("out.png", space_id=brand.id, client_key="results/job-7/out.png")
# ...later, in a different process, with no stored FluidCloud id:
f = fc.files.resolve("results/job-7/out.png")
print(f.id, f.scan_status)Organizing and lifecycle
files = fc.files.list(brand.id) # Space root
files = fc.files.list(brand.id, folder_id=inbox.id) # inside a folder
trashed = fc.files.list(brand.id, trash=True) # soft-deleted
f = fc.files.get(asset.id)
fc.files.rename(asset.id, "logo-final.png")
fc.files.move(asset.id, folder_id=None) # None = Space root
fc.files.delete(asset.id) # soft-delete to trash
fc.files.restore(asset.id)delete is a soft-delete and restore undoes it. See Deleting data for retention behavior.
Links
There are three ways to get a URL for a file:
# 1. Owner download — a short-lived presigned GET, for fetching the bytes yourself.
url = fc.files.download_url(asset.id) # -> str
# 2. Permanent public hotlink — never expires until revoked; good for <img>/embeds.
link = fc.files.public_url(asset.id) # -> Link
print(link.url) # https://files.fluidadmin.com/s/<token>
# 3. Expiring share link — 1..365 days.
link = fc.files.signed_url(asset.id, expires_in_days=7) # -> Link
print(link.url, link.expires_at)public_url and signed_url both create a share link (POST /shares) and require shares:write. The file must be scanned clean first or you get a 409 ConflictError. See Raw links and the Shares reference.
Two known limitations of serve-time behavior (carried from the API, not the SDK): a share-link password is accepted and stored but is not enforced when the file is served; and a revoked permanent link can still serve from edge cache until its max-age of 3600 seconds lapses. See Security.
fc.shares
List and revoke the share links you have minted. Requires shares:read / shares:write. The link token itself is never returned by list — only metadata.
| Method | Endpoint | Returns |
|---|---|---|
fc.shares.list(*, file_id=None, include_inactive=False) | GET /shares | list[Share] |
fc.shares.revoke(share_id) | DELETE /shares/{share_id} | None |
shares = fc.shares.list() # active links across your files
for_file = fc.shares.list(file_id=asset.id) # links for one file
everything = fc.shares.list(include_inactive=True) # include revoked/expired
fc.shares.revoke(shares[0].id)See Shares reference.
fc.quota
Read your current storage and share-link usage against your plan limits. Requires quota:read.
| Method | Endpoint | Returns |
|---|---|---|
fc.quota.usage() | GET /quota | Quota |
q = fc.quota.usage()
print(q.bytes_used, "/", q.bytes_limit)
print(q.links_used, "/", q.links_limit)A limit of -1 means unlimited. See Quota & usage and the Quota reference.
Result objects
Methods return small dataclasses that mirror the API response shapes. UUIDs and timestamps are strings (ISO-8601 UTC). Each object ignores unknown fields, so a server that adds a field will not break an older SDK.
Space
| Field | Type | Notes |
|---|---|---|
id | str | UUID |
name | str | |
created_at, updated_at | str | None | ISO-8601 UTC |
Folder
| Field | Type | Notes |
|---|---|---|
id | str | UUID |
name | str | |
space_id, parent_id | str | None | parent_id is None at the Space root |
deleted_at | str | None | set when soft-deleted |
created_at, updated_at | str | None |
File
| Field | Type | Notes |
|---|---|---|
id | str | UUID |
original_name | str | the stored filename |
space_id, folder_id | str | None | |
mime | str | None | MIME type |
size | int | None | bytes |
sha256 | str | None | content hash |
scan_status | str | None | clean once virus-scanned and safe to share/serve |
status | str | None | lifecycle status |
client_key | str | None | your own logical key, if you set one on upload |
deleted_at | str | None | set when soft-deleted |
created_at, updated_at | str | None | |
public_url | str | None | set only when you upload with public=True |
Link
The create response from public_url / signed_url.
| Field | Type | Notes |
|---|---|---|
url | str | the raw file URL (https://files.fluidadmin.com/s/<token>) |
id | str | the share id (pass to shares.revoke) |
permission | str | defaults to "view" |
expires_at | str | None | None for a permanent public link; an ISO-8601 string for an expiring one |
Share
A listed link from shares.list — the token itself is never included.
| Field | Type | Notes |
|---|---|---|
id | str | share id |
file_id | str | |
permission | str | |
expires_at | str | None | |
max_downloads | int | None | |
download_count | int | |
revoked | bool | |
has_password | bool | a password may be set but is not enforced at serve time |
created_at, updated_at | str | None |
Quota
| Field | Type | Notes |
|---|---|---|
bytes_used | int | storage consumed |
bytes_limit | int | storage cap; -1 = unlimited |
links_used | int | active share links |
links_limit | int | share-link cap; -1 = unlimited |
Errors
The client raises a typed exception for every non-success status. Import them from the package:
from fluidcloud import (
ApiError, # base class for all of them
AuthError, # 401 — missing/invalid/revoked key
QuotaExceededError, # 402 — over storage or share-link cap
PermissionError, # 403 — insufficient_scope or feature_not_in_plan
NotFoundError, # 404 — not found, or not yours (cross-tenant ids are 404)
ConflictError, # 409 — file not ready (not yet scanned clean)
)| Exception | HTTP status | When |
|---|---|---|
AuthError | 401 | Missing, invalid, or revoked key |
QuotaExceededError | 402 | detail == "quota_exceeded" — over storage or share-link cap |
PermissionError | 403 | The key lacks a required scope, or your subscription does not include API access |
NotFoundError | 404 | The resource does not exist, or belongs to another tenant |
ConflictError | 409 | The file is not yet scanned clean and cannot be shared/served |
ApiError | any other (400, 413, 422, 429, …) | Catch-all base; also raised directly for the above codes |
Every exception carries the HTTP status and the parsed detail from the response body. Handle the specific ones you care about and let ApiError cover the rest:
from fluidcloud import QuotaExceededError, ConflictError, ApiError
try:
link = fc.files.public_url(asset.id)
except ConflictError:
print("File still being scanned — retry shortly.")
except QuotaExceededError:
print("Share-link cap reached — revoke an old link or upgrade.")
except ApiError as e:
print("Unexpected API error:", e)429 rate-limit responses surface as ApiError and include a Retry-After header on the underlying response; back off and retry. See Errors and Rate limits.
Full worked example
Create a Space, upload an asset, mint a permanent hotlink, check usage, and clean up.
from fluidcloud import FluidCloud, ConflictError, QuotaExceededError
with FluidCloud(api_key="fck_live_...") as fc:
# 1. Create a Space and a folder.
space = fc.spaces.create("Job Outputs")
folder = fc.folders.create("job-7", space_id=space.id)
# 2. Upload — tag it with our own logical key so we can find it later.
asset = fc.files.upload(
"out.png",
space_id=space.id,
folder_id=folder.id,
client_key="results/job-7/out.png",
)
print("stored as", asset.id, "scan:", asset.scan_status)
# 3. Mint a permanent public hotlink for embedding.
try:
link = fc.files.public_url(asset.id)
print("hotlink:", link.url) # https://files.fluidadmin.com/s/<token>
except ConflictError:
print("Not scanned clean yet — try again in a moment.")
# 4. Later, in another process, fetch by client_key — no stored id needed.
same = fc.files.resolve("results/job-7/out.png")
assert same.id == asset.id
# 5. Check usage.
q = fc.quota.usage()
print(f"storage {q.bytes_used}/{q.bytes_limit}, links {q.links_used}/{q.links_limit}")
# 6. Clean up (soft-delete; restorable from trash).
fc.files.delete(asset.id)Generating a client from the OpenAPI spec (optional)
If you prefer to generate your own typed client instead of using this SDK, the API publishes an OpenAPI specification. Any standard OpenAPI code generator can produce a client from it; remember to send your key in the X-API-Key header. For most integrations the fluidcloud package is the simplest path.
See also
- Quickstart — your first request in a few lines.
- Authentication — keys, headers, and scopes.
- Concepts — Spaces, folders, files, links, scanning.
- TypeScript SDK — the same surface for Node/browsers.
- API reference — raw HTTP endpoints.