Skip to content

TypeScript SDK

The official TypeScript / JavaScript client for the FluidCloud Partner API. It wraps the REST API described in the API reference: it attaches your API key, hides the presign → direct-PUT → complete upload flow behind a single files.upload() call, and maps HTTP errors to typed exceptions.

The SDK talks to the same endpoints documented in the reference, so anything you can do here you can also do with raw HTTP.


Install

bash
npm install fluidcloud

Requirements:

  • Node 18+ (the SDK uses the global fetch and crypto.subtle; both ship with Node 18+). It also runs in modern browsers and edge runtimes that expose fetch.
  • The package is ESM. Import it with import, or use a bundler / "type": "module". In CommonJS, load it with dynamic import().

You need an API key. Keys are created in the FluidCloud dashboard under Settings → Developer → API keys (an active subscription is required). They cannot be created via the API. Keep keys server-side — see Security.


Construct the client

ts
import { FluidCloud } from "fluidcloud";

const fc = new FluidCloud({ apiKey: process.env.FLUIDCLOUD_API_KEY! });

The constructor takes a single options object (FluidCloudOptions):

OptionTypeDefaultNotes
apiKeystring— (required)Your key (fck_live_… / fck_test_…). Sent as X-API-Key. Throws if missing.
baseUrlstringhttps://api-cloud.fluidvip.comAPI origin. The SDK appends /api/v1 for you. Trailing slashes are trimmed.
apiVersionstring"v1"API version segment. Leave at the default.
fetchtypeof fetchthe runtime's global fetchOverride the fetch implementation (tests, or runtimes without a global fetch).

The client picks the auth header from your key: keys starting with fck_ are sent as X-API-Key; otherwise the value is sent as Authorization: Bearer …. See Authentication.

If your runtime has no global fetch and you do not pass options.fetch, the constructor throws.


Resources

The client exposes five resource namespaces:

NamespaceClassWhat it covers
fc.spacesSpacesTop-level containers (Spaces)
fc.foldersFoldersFolder tree inside a space (Folders)
fc.filesFilesUpload, list, fetch, move, delete, share files (Files, Uploads)
fc.sharesSharesList and revoke share links (Shares)
fc.quotaQuotaResourceRead storage + share-link usage (Quota)

Every method returns a Promise. All ids are UUID strings; all timestamps are ISO-8601 UTC strings.


fc.spaces

ts
fc.spaces.list(): Promise<Space[]>
fc.spaces.create(name: string): Promise<Space>
MethodHTTPScopeReturns
list()GET /spacesspaces:readSpace[]
create()POST /spacesspaces:writeSpace
ts
const spaces = await fc.spaces.list();
const space = await fc.spaces.create("partner-prod");

fc.folders

ts
fc.folders.list(
  spaceId: string,
  parentId?: string | null,        // default null = root of the space
  opts?: { trash?: boolean },      // true = list trashed folders
): Promise<Folder[]>

fc.folders.create(
  name: string,
  spaceId: string,
  parentId?: string | null,        // default null = root
): Promise<Folder>

fc.folders.rename(folderId: string, name: string): Promise<Folder>
fc.folders.move(folderId: string, parentId: string | null): Promise<Folder>
fc.folders.delete(folderId: string): Promise<Folder>     // soft-delete (to trash)
fc.folders.restore(folderId: string): Promise<Folder>    // restore from trash
MethodHTTPScopeReturns
list()GET /foldersfolders:readFolder[]
create()POST /foldersfolders:writeFolder
rename()PATCH /folders/{id}folders:writeFolder
move()PATCH /folders/{id}folders:writeFolder
delete()DELETE /folders/{id}folders:writeFolder
restore()POST /folders/{id}/restorefolders:writeFolder

delete() is a soft-delete: the folder moves to trash and restore() brings it back. See Organizing files and Deleting data.

ts
const folder = await fc.folders.create("job-7", space.id);
const subfolder = await fc.folders.create("out", space.id, folder.id);
await fc.folders.rename(folder.id, "job-7-archive");
await fc.folders.move(subfolder.id, null);  // move to space root

fc.files

ts
// Upload (presign -> direct PUT -> complete, incl. multipart) — see below
fc.files.upload(
  file: UploadInput,
  spaceId: string,
  options?: UploadOptions,
): Promise<FileItem>

fc.files.list(
  spaceId: string,
  folderId?: string | null,                 // default null = space root
  opts?: { trash?: boolean },
): Promise<FileItem[]>

fc.files.get(fileId: string): Promise<FileItem>
fc.files.resolve(clientKey: string): Promise<FileItem>   // look up by YOUR client_key

fc.files.rename(fileId: string, name: string): Promise<FileItem>
fc.files.move(fileId: string, folderId: string | null): Promise<FileItem>
fc.files.delete(fileId: string): Promise<FileItem>       // soft-delete (to trash)
fc.files.restore(fileId: string): Promise<FileItem>      // restore from trash

fc.files.downloadUrl(fileId: string): Promise<string>    // short-lived presigned GET

// Share helpers (mint links)
fc.files.publicUrl(fileId: string): Promise<Link>        // permanent public hotlink
fc.files.signedUrl(
  fileId: string,
  opts?: { expiresInDays?: number; permission?: "view" | "download" },
): Promise<Link>                                          // expiring link (default 7 days, "view")
MethodHTTPScopeReturns
upload()POST /uploads/initiate → PUT(s) → POST /uploads/completefiles:writeFileItem
list()GET /filesfiles:readFileItem[]
get()GET /files/{id}files:readFileItem
resolve()GET /files/resolvefiles:readFileItem
rename()PATCH /files/{id}files:writeFileItem
move()PATCH /files/{id}files:writeFileItem
delete()DELETE /files/{id}files:writeFileItem
restore()POST /files/{id}/restorefiles:writeFileItem
downloadUrl()GET /files/{id}/downloadfiles:readstring
publicUrl()POST /sharesshares:writeLink
signedUrl()POST /sharesshares:writeLink

Notes:

  • resolve(clientKey) fetches the file you uploaded under your own logical client_key (e.g. results/job-7/out.png) without storing FluidCloud's file id. If no file matches the key it throws NotFoundError. See UploadOptions.clientKey below.
  • downloadUrl() returns a short-lived presigned GET URL for fetching the raw bytes yourself (not a shareable link).
  • publicUrl() / signedUrl() mint share links and require the shares:write scope. publicUrl() makes a permanent, cacheable, inline hotlink (expires_at: null); signedUrl() makes an expiring one (1–365 days). The minted url is served from the cookieless raw-link domain, https://files.fluidadmin.com/s/<token>. Both succeed only once the file is scanned clean — see Quarantine-until-clean. For an overview of link types see Raw links.
ts
const f = await fc.files.get(fileId);
const files = await fc.files.list(space.id, folder.id);

// Stable hotlink:
const { url } = await fc.files.publicUrl(f.id);
// -> https://files.fluidadmin.com/s/...

// 30-day download link:
const link = await fc.files.signedUrl(f.id, { expiresInDays: 30, permission: "download" });

fc.shares

ts
fc.shares.list(
  opts?: { fileId?: string; includeInactive?: boolean },
): Promise<Share[]>

fc.shares.revoke(shareId: string): Promise<void>
MethodHTTPScopeReturns
list()GET /sharesshares:readShare[]
revoke()DELETE /shares/{id}shares:writevoid

list() returns active links by default; pass includeInactive: true to include revoked / expired ones, or fileId to filter to one file. See Shares.

Revoking a permanent public link stops new lookups, but already-cached responses can still be served from the edge for up to 3600 seconds (its max-age). Plan around this before relying on revocation for hard cut-off. A share-link password is accepted and stored (has_password) but is not enforced at serve time — do not rely on it as an access control.

ts
const shares = await fc.shares.list({ fileId: f.id });
await fc.shares.revoke(shares[0].id);

fc.quota

ts
fc.quota.usage(): Promise<Quota>
MethodHTTPScopeReturns
usage()GET /quotaquota:readQuota

Quota reports bytes_used / bytes_limit and links_used / links_limit. A limit of -1 means unlimited. See Quota & usage.

ts
const q = await fc.quota.usage();
const pct =
  q.bytes_limit === -1 ? 0 : (q.bytes_used / q.bytes_limit) * 100;
console.log(`storage ${pct.toFixed(1)}%, links ${q.links_used}/${q.links_limit}`);

Uploading

fc.files.upload() runs the whole flow for you: it asks the API for a ticket (POST /uploads/initiate), PUTs your bytes directly to storage via the presigned URL(s) — for large files it transparently does a multipart upload across the returned part URLs — and then calls POST /uploads/complete. The API never proxies your bytes.

ts
async upload(
  file: UploadInput,
  spaceId: string,
  options?: UploadOptions,
): Promise<FileItem>

The uploaded file is automatically scanned. It returns immediately with scan_status possibly still "pending"; you must wait for "clean" before you can share or serve it (see below).

Upload input types

ts
type UploadInput = Blob | ArrayBuffer | Uint8Array;
  • Blob / File — browser File/Blob, or Node 18+ global Blob. Its name and type are used when present.
  • ArrayBuffer / Uint8Array — raw bytes (Node or browser).

When the input carries no name/type, pass them via options. The SDK guesses a content type from the file extension and falls back to application/octet-stream.

ts
interface UploadOptions {
  folderId?: string | null;   // target folder; default = space root
  name?: string;              // original filename (for raw bytes / overrides)
  contentType?: string;       // MIME type override
  public?: boolean;           // also mint a permanent hotlink (sets result.public_url)
  clientKey?: string;         // YOUR logical key; later fetch via files.resolve()
}

When public: true, the SDK additionally mints a permanent public hotlink and sets it on the returned FileItem.public_url — saving you a separate publicUrl() call. (Minting the link requires the shares:write scope.)

ts
// From raw bytes in Node:
const item = await fc.files.upload(buf, space.id, {
  name: "out.png",
  contentType: "image/png",
  folderId: folder.id,
  clientKey: "results/job-7/out.png",
  public: true,
});
console.log(item.public_url); // https://files.fluidadmin.com/s/...

// Later, without storing item.id:
const same = await fc.files.resolve("results/job-7/out.png");

See Uploading for the full guide and Uploads for the underlying endpoints.

Quarantine-until-clean

Every upload is virus-scanned. A file must reach scan_status === "clean" before it can be shared or served; until then, publicUrl(), signedUrl(), and serving a link return 409ConflictError. Poll fc.files.get(id) until the status is clean:

ts
async function waitClean(fc: FluidCloud, fileId: string): Promise<FileItem> {
  for (;;) {
    const f = await fc.files.get(fileId);
    if (f.scan_status === "clean") return f;
    if (f.scan_status === "infected") throw new Error("file failed virus scan");
    await new Promise((r) => setTimeout(r, 1000));
  }
}

Result types

The SDK returns plain objects mirroring the API responses. Key fields (all *_at are ISO-8601 UTC strings; ids are UUIDs):

ts
interface Space   { id; name; tenant_id?; created_at?; updated_at? }

interface Folder  { id; name; space_id?; parent_id?: string | null;
                    deleted_at?: string | null; created_at?; updated_at? }

type ScanStatus = "pending" | "clean" | "infected";

interface FileItem {
  id;
  original_name;
  name?;
  space_id?;
  folder_id?: string | null;
  mime?: string | null;
  size?: number | null;
  sha256?: string | null;
  scan_status?: ScanStatus;          // must be "clean" to share/serve
  status?;
  client_key?: string | null;        // your logical key, if you set one
  deleted_at?: string | null;
  created_at?; updated_at?;
  public_url?;                        // set when upload(..., { public: true })
}

interface Link  {                     // returned by publicUrl()/signedUrl()
  url;                                // https://files.fluidadmin.com/s/<token>
  id;
  permission;
  expires_at: string | null;          // null = permanent
}

interface Share {                     // returned by shares.list()
  id; file_id; permission;
  expires_at: string | null;
  max_downloads: number | null;
  download_count: number;
  revoked: boolean;
  has_password: boolean;              // stored, NOT enforced at serve time
  created_at?; updated_at?;
}

interface Quota {
  bytes_used; bytes_limit;            // limit -1 = unlimited
  links_used; links_limit;            // limit -1 = unlimited
}

Error handling

Every non-2xx response is thrown as a typed error. Import the classes and branch on instanceof:

ts
import {
  FluidCloud,
  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)
  ConflictError,       // 409 — file not ready (quarantine-until-clean)
  ApiError,            // any other status (400, 413, 422, 429, 5xx, ...)
} from "fluidcloud";

try {
  const link = await fc.files.publicUrl(fileId);
  console.log(link.url);
} catch (err) {
  if (err instanceof ConflictError) {
    // file is still being scanned — retry after it's clean
  } else if (err instanceof QuotaExceededError) {
    // over storage or share-link limit
  } else if (err instanceof PermissionError) {
    // key lacks the scope, or the plan doesn't include API access
  } else if (err instanceof NotFoundError) {
    // wrong id, or an id from another tenant
  } else if (err instanceof AuthError) {
    // rotate/replace the key
  } else if (err instanceof ApiError) {
    console.error(err.status, err.detail);  // 400/413/422/429/5xx
    throw err;
  } else {
    throw err;
  }
}

AuthError, QuotaExceededError, PermissionError, NotFoundError, and ConflictError all extend ApiError, so a single catch (err: unknown) { if (err instanceof ApiError) ... } covers every API failure. The error carries the HTTP status and the response body's detail.

A 429 (rate limited) surfaces as ApiError with status === 429; honor the Retry-After header before retrying. See Errors and Rate limits.


Full example

ts
import {
  FluidCloud,
  ConflictError,
  ApiError,
} from "fluidcloud";
import { readFile } from "node:fs/promises";

const fc = new FluidCloud({ apiKey: process.env.FLUIDCLOUD_API_KEY! });

// 1. A space + folder to organize into.
const [space] = await fc.spaces.list();
const folder = await fc.folders.create("job-7", space.id);

// 2. Upload raw bytes, tagged with your own logical key.
const bytes = await readFile("./out.png");
let file = await fc.files.upload(bytes, space.id, {
  name: "out.png",
  folderId: folder.id,
  clientKey: "results/job-7/out.png",
});

// 3. Wait until the virus scan clears.
while (file.scan_status !== "clean") {
  if (file.scan_status === "infected") throw new Error("failed scan");
  await new Promise((r) => setTimeout(r, 1000));
  file = await fc.files.get(file.id);
}

// 4. Mint a permanent public hotlink and a 7-day signed link.
const hotlink = await fc.files.publicUrl(file.id);
const signed = await fc.files.signedUrl(file.id, { expiresInDays: 7 });
console.log(hotlink.url);  // https://files.fluidadmin.com/s/...
console.log(signed.url, "expires", signed.expires_at);

// 5. Later, resolve by your client_key — no FluidCloud id needed.
const again = await fc.files.resolve("results/job-7/out.png");

// 6. Check usage.
const q = await fc.quota.usage();
console.log(`${q.bytes_used}/${q.bytes_limit} bytes, ${q.links_used}/${q.links_limit} links`);

Notes & limits

  • Direct-to-storage uploads. Bytes go straight to object storage over presigned URLs; the API only issues the ticket and records completion. Large files upload as multipart automatically.
  • Scopes. A partner key carries a fixed scope set (spaces, folders, files, shares read/write, plus quota:read). A call lacking a required scope throws PermissionError (insufficient_scope). See Authentication.
  • Rate limits. 600 requests/min per key by default, plus per-action caps (share links 120/min, upload starts 300/min). Over a limit → ApiError with status === 429 and a Retry-After header. See Rate limits.
  • Federation / cross-service delegation (acting as another user) is a first-party capability and is not available to partner API keys; those helpers are intentionally not part of your integration surface.

See also: Python SDK · Quickstart · Concepts · API reference.

FluidCloud API — part of the Fluidvip ecosystem.