FileNest/Docs

Python SDK

The filenest Python SDK provides both synchronous (FileNest) and asynchronous (AsyncFileNest) clients. All namespaces — files, folders, uploads, upload tokens, webhooks, and search — are available on both.

Installation

pip install filenest

Setup

from filenest import FileNest, AsyncFileNest
 
# Synchronous
fn = FileNest(
    api_key="fn_live_...",
    project_id="proj_01j...",
)
 
# Asynchronous (context manager recommended)
async with AsyncFileNest(api_key="fn_live_...", project_id="proj_01j...") as fn:
    ...

Files

Upload

# From bytes
with open("report.pdf", "rb") as f:
    file = fn.files.upload(
        filename="report.pdf",
        data=f.read(),
        mime_type="application/pdf",
        folder_id="fld_01j...",     # optional
        tags=["finance", "2026"],   # optional
        metadata={"year": "2026"},  # optional
    )
 
# From a filesystem path (MIME type auto-detected)
file = fn.files.upload_from_path(
    path="./report.pdf",
    folder_id="fld_01j...",
    tags=["finance"],
)

Files under 5 MB are uploaded in a single request. Files over 5 MB use the multipart endpoint automatically — no configuration needed.

Download

# Get a presigned URL
url_info = fn.files.get_download_url(file.id, ttl=3600)
print(url_info.url)
 
# Save directly to disk
fn.files.download_to_path(file.id, dest="./output.pdf")
 
# Load into memory
content = fn.files.download_to_bytes(file.id)

List

result = fn.files.list(
    status="ready",
    mime_type="application/pdf",
    tags=["finance"],
    sort_by="created_at",
    sort_order="desc",
    limit=50,
    offset=0,
)
for f in result.items:
    print(f.filename)

Get, update, delete, restore

file = fn.files.get(file_id)
 
fn.files.update(
    file_id,
    filename="report-final.pdf",
    tags=["archived"],
    metadata={"reviewed": True},
)
 
fn.files.delete(file_id)   # soft delete — recoverable
 
# Restore a soft-deleted file
file = fn.files.restore(file_id)

Versions

versions = fn.files.list_versions(file_id)
 
# Restore to a previous version
fn.files.restore_version(file_id, version_id)

Folders

Create and ensure paths

# Create a single folder
folder = fn.folders.create("invoices")
folder = fn.folders.create("q1", parent_folder_id=folder["id"], metadata={"dept": "finance"})
 
# Idempotent: create every missing path segment, return the leaf folder
folder = fn.folders.ensure_path("users/alice/uploads")

Resolve paths

# Returns None if the path does not exist
folder = fn.folders.get_by_path("users/alice/uploads")
 
folder = fn.folders.get(folder_id)

List and browse

result = fn.folders.list()
result = fn.folders.list(name="uploads")   # name filter
 
# List files inside a folder with filters
files = fn.folders.list_files(
    folder_id,
    q="invoice",           # filename search
    tags=["finance"],
    category="document",
    status="ready",
    limit=20,
    cursor=last_cursor,    # cursor-based pagination
)

Delete

# Fails with an error if the folder contains files or subfolders
fn.folders.delete(folder_id)

Resumable Uploads

fn.uploads gives you manual control over the multipart session lifecycle. Use this when you need to resume an interrupted upload. For most cases, fn.files.upload() handles multipart automatically.

# 1. Create a session and store upload_id somewhere durable
session = fn.uploads.create(
    filename="large-video.mp4",
    size_bytes=len(data),
    mime_type="video/mp4",
    folder_id="fld_01j...",
    tags=["video", "raw"],
    metadata={"uploaded_by": "user_abc"},
)
# session["upload_id"] → persist this to resume later
 
# 2. Upload all parts (or resume from where you left off)
file = fn.uploads.resume(
    session["upload_id"],
    data=data,
    on_progress=lambda pct: print(f"{pct}%"),
)
 
# 3. Abort if no longer needed
fn.uploads.abort(session["upload_id"])

Upload Tokens

token = fn.upload_tokens.create(
    folder_id=folder["id"],
    owner_user_id="user_abc",
    owner_org_id="org_xyz",
    max_size=50 * 1024 * 1024,                           # 50 MB per file
    max_files=10,
    allowed_mime_types=["image/*", "application/pdf"],
    metadata={"uploaded_from": "profile-settings"},      # stamped on every file
    tags=["avatar", "user-content"],                     # merged onto every file
    expires_in=3600,
)
print(token.token)       # fn_upload_token_...  — return this to the browser
print(token.expires_at)  # ISO timestamp — return this for client-side refresh

Token constraints are validated against the project config at creation time. See Upload Tokens API reference for constraint reconciliation rules.


Webhooks

# Create an endpoint
wh = fn.webhooks.create(
    name="prod-receiver",
    url="https://app.example.com/webhooks/filenest",
    events=["file.uploaded", "file.ready", "file.failed"],
)
# wh["signing_secret"] — shown only once, store safely
 
result = fn.webhooks.list()
wh = fn.webhooks.get(webhook_id)
fn.webhooks.update(webhook_id, is_active=False)
fn.webhooks.delete(webhook_id)
 
deliveries = fn.webhooks.list_deliveries(webhook_id, limit=20)
 
# Verify a payload
is_valid = fn.webhooks.verify(raw_body, signature_header, secret)

Standalone import (no client instance needed):

from filenest.namespaces.webhooks import verify_webhook_signature
 
is_valid = verify_webhook_signature(raw_body, signature_header, secret)

results = fn.search.query(
    q="invoice",
    filters={"status": "ready", "tags": ["finance"]},
    facets=["mime_type", "tags"],
    sort_by="created_at",
    sort_order="desc",
    limit=20,
)
 
# Iterate over all results with automatic pagination
for file in fn.search.iterate(q="invoice"):
    print(file.filename)

FastAPI integration

from fastapi import FastAPI, UploadFile, Request, HTTPException
from filenest import AsyncFileNest
from filenest.namespaces.webhooks import verify_webhook_signature
 
app = FastAPI()
fn = AsyncFileNest(api_key="fn_live_...", project_id="proj_01j...")
 
 
@app.post("/upload")
async def upload(file: UploadFile, user_id: str):
    folder = await fn.folders.ensure_path(f"users/{user_id}/uploads")
    result = await fn.files.upload(
        filename=file.filename,
        data=await file.read(),
        mime_type=file.content_type,
        folder_id=folder["id"],
    )
    return {"file_id": result.id, "status": result.status}
 
 
@app.post("/webhooks/filenest")
async def webhook(request: Request):
    body = await request.body()
    sig = request.headers.get("x-filenest-signature", "")
    if not verify_webhook_signature(body, sig, secret="..."):
        raise HTTPException(status_code=401, detail="Invalid signature")
    event = await request.json()
    print(event["type"], event["data"])
    return {"ok": True}

Async client

Every method on AsyncFileNest is the async counterpart — same signatures, just await them:

async with AsyncFileNest(api_key="fn_live_...", project_id="proj_...") as fn:
    folder = await fn.folders.ensure_path("users/alice/uploads")
 
    token = await fn.upload_tokens.create(
        folder_id=folder["id"],
        owner_user_id="alice",
        tags=["user-content"],
        expires_in=3600,
    )
 
    files = await fn.folders.list_files(folder["id"], limit=20)
 
    # Resumable multipart
    session = await fn.uploads.create(filename="video.mp4", size_bytes=len(data))
    file = await fn.uploads.resume(session["upload_id"], data=data)