Skip to content

API Reference — Uploads

Endpoints for getting bytes into FluidCloud. The API never proxies your file bytes: you ask FluidCloud for a presigned upload ticket, then PUT the bytes directly to storage from your own client. A short /complete call then registers the file. There is also a server-side URL import that fetches a remote file for you.

All routes are under the base URL https://api-cloud.fluidvip.com/api/v1 and are prefixed with /uploads. Every request must carry your API key — see authentication.md.

For the end-to-end walkthrough (single-PUT vs. multipart, the SDK upload() shortcut, and progress handling), read the Uploading guide.

Method & pathScopePurpose
POST /uploads/initiatefiles:writeReserve a file id + key, get presigned upload ticket(s)
POST /uploads/completefiles:writeFinalize the upload and create the file record
POST /uploads/import-urlfiles:writeServer-side fetch of a remote URL into a Space

Quarantine-until-clean. Every uploaded or imported file is automatically virus-scanned. A new file lands with scan_status pending and status draft; it becomes scan_status clean (and status advances) only after the scan passes. A file must be clean before it can be shared or served — sharing a not-yet-clean file returns 409. See Raw links and Shares.


The upload flow

A direct-to-storage upload is three steps:

  1. POST /uploads/initiate — you describe the file (name, size, Space). FluidCloud reserves a file_id and a storage key, then returns a presigned ticket. No file record exists yet.
  2. You PUT the bytes to the presigned URL(s) returned by step 1 — straight to storage, not through this API.
  3. POST /uploads/complete — you confirm the upload (echoing the file_id and key, plus multipart ETags if any). FluidCloud creates the file record, counts the bytes against your quota, and runs the scan.

The cutoff between single and multipart mode is 100 MB, and FluidCloud decides it for you from the size you send to /initiate:

  • Below 100 MB → mode: "single". One presigned PUT URL (upload_url).
  • 100 MB and above → mode: "multipart". An upload_id, a part_size, and one presigned URL per part (part_urls). Upload each part, collect its ETag, and pass all parts back to /complete.

Tip — let the SDK do it. files.upload (Python) / files.upload (TypeScript) runs all three steps for you, picks single vs. multipart, uploads the parts, and returns the finished file. Use the raw endpoints below only when you need fine-grained control (e.g. your own resumable part scheduler). See Python SDK and TypeScript SDK.


POST /uploads/initiate

Reserve a file id and a quarantine storage key, and receive presigned upload ticket(s). No file record is created yet — that happens at /complete.

Scope: files:write

Request body

FieldTypeRequiredNotes
original_namestringyesThe file's display name (min length 1). Used to derive the file extension.
sizeintegeryesExact byte size of the file (>= 0). Determines single vs. multipart and is checked against your storage quota.
space_idstring (UUID)yesThe Space the file will belong to. See Spaces.
content_typestringnoMIME type. In single mode this exact value is signed into the ticket — your PUT must send the same Content-Type header (or none, if you omit it), or storage rejects the PUT.
folder_idstring (UUID)noDestination folder within the Space. Omit for the Space root. See Folders.

Response — InitiateResponse

FieldTypeNotes
file_idstring (UUID)Reserved id. Echo it back to /complete.
keystringServer-built storage key under your tenant's quarantine prefix. Echo it back to /complete (it is re-validated, never trusted blindly).
mode"single" | "multipart"Which flow to use.
content_typestring | nullThe Content-Type that was signed (single mode). Send it verbatim on your PUT. null = none signed.
upload_urlstring | nullSingle mode only. Presigned PUT URL for the whole file.
upload_idstring | nullMultipart mode only. Pass back to /complete.
part_sizeinteger | nullMultipart mode only. Byte size of each non-final part. Slice the file into parts of exactly this size (the last part may be smaller).
part_urlsarray | nullMultipart mode only. List of { part_number, url }. PUT each part to its url.

Status codes

  • 200 — ticket issued.
  • 400 — bad input (e.g. malformed space_id/folder_id).
  • 401 — missing/invalid/revoked key.
  • 402detail is quota_exceeded. Storing size more bytes would exceed your storage cap; nothing is signed. Check Quota.
  • 403insufficient_scope (key lacks files:write), or you don't have access to the target folder.
  • 404 — the space_id/folder_id is not found or not yours.
  • 413 — the file is too large to upload (would exceed the multipart part cap).
  • 422 — request body validation failed.
  • 429 — too many /initiate calls; the per-action cap is 300/min. A Retry-After header is returned. See Rate limits.

Examples

curl (single, < 100 MB)

bash
curl -X POST https://api-cloud.fluidvip.com/api/v1/uploads/initiate \
  -H "X-API-Key: fck_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "original_name": "report.pdf",
    "content_type": "application/pdf",
    "size": 824113,
    "space_id": "8a1f0e2c-1234-4abc-9def-0123456789ab"
  }'

Response:

json
{
  "file_id": "0c3f9b7a-aaaa-4bbb-8ccc-111122223333",
  "key": "…/incoming/…/report.pdf",
  "mode": "single",
  "content_type": "application/pdf",
  "upload_url": "https://…storage…/…?X-Amz-Signature=…",
  "upload_id": null,
  "part_size": null,
  "part_urls": null
}

Then PUT the bytes (note the matching Content-Type):

bash
curl -X PUT "https://…storage…/…?X-Amz-Signature=…" \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf

Python (fluidcloud)

python
from fluidcloud import FluidCloud

fc = FluidCloud(api_key="fck_live_...")

ticket = fc.uploads.initiate(
    original_name="report.pdf",
    content_type="application/pdf",
    size=824113,
    space_id="8a1f0e2c-1234-4abc-9def-0123456789ab",
)
print(ticket.mode, ticket.upload_url)

# In practice, prefer the one-call helper, which handles initiate -> PUT -> complete:
file = fc.files.upload("report.pdf", space_id="8a1f0e2c-1234-4abc-9def-0123456789ab")
print(file.id, file.scan_status)

TypeScript (fluidcloud)

ts
import { FluidCloud } from "fluidcloud";

const fc = new FluidCloud({ apiKey: "fck_live_..." });

const ticket = await fc.uploads.initiate({
  original_name: "report.pdf",
  content_type: "application/pdf",
  size: 824113,
  space_id: "8a1f0e2c-1234-4abc-9def-0123456789ab",
});
console.log(ticket.mode, ticket.upload_url);

// Or the one-call helper (initiate -> PUT -> complete):
const file = await fc.files.upload(blob, {
  space_id: "8a1f0e2c-1234-4abc-9def-0123456789ab",
});
console.log(file.id, file.scan_status);

POST /uploads/complete

Finalize the upload — completing the multipart upload (if any) and creating the file record. The bytes are counted against your storage quota in the same transaction, then the file is scanned.

Scope: files:write

You must echo the file_id and key from /initiate. The key is re-validated against your tenant prefix; a key that doesn't belong to you is rejected.

Request body

FieldTypeRequiredNotes
file_idstring (UUID)yesThe file_id from /initiate.
keystringyesThe key from /initiate.
original_namestringyesDisplay name (min length 1).
sizeintegeryesFinal byte size (>= 0).
space_idstring (UUID)yesSame Space as /initiate.
upload_idstringconditionalMultipart only. The upload_id from /initiate.
partsarrayconditionalMultipart only. List of { "PartNumber": <int >= 1>, "ETag": "<etag>" }, one per uploaded part. Required when upload_id is present.
mimestringnoMIME type to store as metadata.
sha256stringnoHex SHA-256 of the file, stored as metadata.
folder_idstring (UUID)noDestination folder. Re-checked here.
client_keystringnoYour own logical key for this file (e.g. results/job-7/out.png). Lets you fetch it later by your key via files.resolve / GET /files/resolve, without storing FluidCloud's id. See note below.

Response — FileResponse

The created file record:

FieldTypeNotes
idstring (UUID)The file's FluidCloud id.
tenant_idstring (UUID)Your tenant.
space_idstring (UUID)
folder_idstring (UUID) | null
source_appstring | nullOriginating product tag.
client_keystring | nullYour logical key, if you sent one.
original_namestring
mimestring | null
sizeinteger | null
sha256string | null
scan_statusstringpending right after completion; becomes clean (or infected) once the scan finishes.
statusstringdraft while quarantined; advances once scanned clean.
created_bystring | null

About client_key. A logical key maps to exactly one live object. If you complete a new upload with the same client_key (within the same Space context), the previous file with that key is superseded (soft-deleted) so files.resolve deterministically returns the newest one. This mirrors object-storage semantics: writing the same key replaces it. See Files.

Status codes

  • 200 — file record created (scan may still be in progress; check scan_status).
  • 400 — bad input: malformed id, a key that doesn't match the tenant/Space prefix, or a multipart completion missing parts.
  • 401 — missing/invalid/revoked key.
  • 403insufficient_scope, or you don't have access to the target folder.
  • 404 — the space_id/folder_id is not found or not yours.
  • 402detail is quota_exceeded (the byte reservation would exceed your storage cap).
  • 422 — request body validation failed.
  • 429 — rate limited (the default 600/min API ceiling). A Retry-After header is returned.

Examples

curl (single — no upload_id/parts)

bash
curl -X POST https://api-cloud.fluidvip.com/api/v1/uploads/complete \
  -H "X-API-Key: fck_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "file_id": "0c3f9b7a-aaaa-4bbb-8ccc-111122223333",
    "key": "…/incoming/…/report.pdf",
    "original_name": "report.pdf",
    "mime": "application/pdf",
    "size": 824113,
    "space_id": "8a1f0e2c-1234-4abc-9def-0123456789ab",
    "client_key": "results/job-7/report.pdf"
  }'

curl (multipart — with upload_id and parts)

bash
curl -X POST https://api-cloud.fluidvip.com/api/v1/uploads/complete \
  -H "X-API-Key: fck_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "file_id": "0c3f9b7a-aaaa-4bbb-8ccc-111122223333",
    "key": "…/incoming/…/movie.mp4",
    "original_name": "movie.mp4",
    "mime": "video/mp4",
    "size": 734003200,
    "space_id": "8a1f0e2c-1234-4abc-9def-0123456789ab",
    "upload_id": "2~abc123…",
    "parts": [
      { "PartNumber": 1, "ETag": "\"d41d8cd9…\"" },
      { "PartNumber": 2, "ETag": "\"0cc175b9…\"" }
    ]
  }'

Python (fluidcloud)

python
file = fc.uploads.complete(
    file_id=ticket.file_id,
    key=ticket.key,
    original_name="report.pdf",
    mime="application/pdf",
    size=824113,
    space_id="8a1f0e2c-1234-4abc-9def-0123456789ab",
    client_key="results/job-7/report.pdf",
    # multipart only:
    # upload_id=ticket.upload_id,
    # parts=[{"PartNumber": 1, "ETag": etag1}, ...],
)
print(file.id, file.scan_status, file.status)

TypeScript (fluidcloud)

ts
const file = await fc.uploads.complete({
  file_id: ticket.file_id,
  key: ticket.key,
  original_name: "report.pdf",
  mime: "application/pdf",
  size: 824113,
  space_id: "8a1f0e2c-1234-4abc-9def-0123456789ab",
  client_key: "results/job-7/report.pdf",
  // multipart only:
  // upload_id: ticket.upload_id,
  // parts: [{ PartNumber: 1, ETag: etag1 }, /* ... */],
});
console.log(file.id, file.scan_status, file.status);

POST /uploads/import-url

Have FluidCloud fetch a file from a remote http(s) URL and store it in a Space — no presign/PUT round-trip on your side. The fetch is size-capped and runs through the same quarantine-and-scan pipeline as a direct upload.

Scope: files:write

The remote source must be a single, directly-downloadable file. The import is capped at 50 GB; the source Content-Type is kept only as display metadata (the real file type is determined by the scan). Importing from a Google Drive folder link is not available to API keys.

Request body

FieldTypeRequiredNotes
urlstringyesPublic http(s) URL of the file (min length 1).
space_idstring (UUID)yesDestination Space.
folder_idstring (UUID)noDestination folder.
namestringnoOverride the stored filename. If omitted, the name is derived from the URL path (or the response's Content-Disposition).

Response — FileResponse

Same shape as /uploads/complete (see above). On success the file exists and has been scanned; check scan_status for the verdict.

Status codes

  • 200 — file imported.
  • 400 — bad input: unresolvable/non-public host, disallowed scheme, an invalid redirect, an empty body, or an HTTP error from the source. (For safety, only public hosts are reachable; internal/private addresses are refused.)
  • 401 — missing/invalid/revoked key.
  • 402detail is quota_exceeded (the import would exceed your storage cap; refused before or after fetch).
  • 403insufficient_scope, or you don't have access to the target folder.
  • 404 — the space_id/folder_id is not found or not yours.
  • 413 — the file exceeds the 50 GB import limit.
  • 422 — request body validation failed.
  • 429 — too many imports started; the per-action cap is 30/min. A Retry-After header is returned.

Examples

curl

bash
curl -X POST https://api-cloud.fluidvip.com/api/v1/uploads/import-url \
  -H "X-API-Key: fck_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/assets/photo.jpg",
    "space_id": "8a1f0e2c-1234-4abc-9def-0123456789ab",
    "name": "photo.jpg"
  }'

Python (fluidcloud)

python
file = fc.uploads.import_url(
    url="https://example.com/assets/photo.jpg",
    space_id="8a1f0e2c-1234-4abc-9def-0123456789ab",
    name="photo.jpg",
)
print(file.id, file.scan_status)

TypeScript (fluidcloud)

ts
const file = await fc.uploads.importUrl({
  url: "https://example.com/assets/photo.jpg",
  space_id: "8a1f0e2c-1234-4abc-9def-0123456789ab",
  name: "photo.jpg",
});
console.log(file.id, file.scan_status);

Error handling

Every error is an HTTP status with a JSON body whose top key is detail (a string, or a structured object for 403). The SDKs map statuses to typed errors: QuotaExceededError (402), PermissionError (403), NotFoundError (404), AuthError (401), ConflictError (409), and ApiError for the rest. For the full model, see Errors.

See also

  • Uploading guide — the full single vs. multipart walkthrough and the SDK upload() shortcut.
  • Files — read, list, resolve-by-client_key, and manage files after upload.
  • Shares and Raw links — turn a scanned-clean file into a public link.
  • Quota — read your storage usage and limits.
  • Rate limits — the 600/min default plus the 300/min initiate and 30/min import caps.

FluidCloud API — part of the Fluidvip ecosystem.