Site icon Filestack Blog

How to Integrate Filestack with Flask Using the Python SDK

Flask gives you a minimal foundation and trusts you to bolt on what you need. That works well until file uploads enter the picture, at which point you are suddenly responsible for storage, a CDN, image processing, virus scanning, and uploads from external sources like Google Drive or Dropbox. Filestack handles that layer, and the official Python SDK plugs into Flask with a few dozen lines of code.

 

There is no official flask-filestack extension on PyPI, but Flask’s request model and Werkzeug’s FileStorage objects are already shaped for this. The Python SDK accepts file-like objects directly, which means the integration is mostly a thin service wrapper, a few blueprints, and a webhook endpoint.

 

Every snippet in this guide runs against the real SDK. Copy them in order and you will have working uploads, signed downloads, transformations, and verified webhooks by the end.

Key takeaways

Before you start

You need:

Pull your API key and app secret from the Filestack developer portal. The API key is fine in your frontend code. The app secret stays on the server and signs every security policy.

Step 1: Install and configure

Install the SDK alongside Flask:

 

pip install filestack-python flask

 

Configuration goes in your Flask config object. Read from environment variables.

 

# config.py

 

import os




class Config:




    SECRET_KEY = os.environ["FLASK_SECRET_KEY"]




    FILESTACK_API_KEY = os.environ["FILESTACK_API_KEY"]




    FILESTACK_APP_SECRET = os.environ["FILESTACK_APP_SECRET"]




    FILESTACK_WEBHOOK_SECRET = os.environ.get("FILESTACK_WEBHOOK_SECRET", "")




    MAX_CONTENT_LENGTH = 100 * 1024 * 1024  # 100 MB upload cap

 

# app.py

 

from flask import Flask




from config import Config




def create_app():




    app = Flask(__name__)




    app.config.from_object(Config)




    from routes.uploads import uploads_bp




    from routes.webhooks import webhooks_bp




    app.register_blueprint(uploads_bp)




    app.register_blueprint(webhooks_bp)




    return app

Set your env vars in your shell or .env file:

 

export FLASK_SECRET_KEY=change-me

 

export FILESTACK_API_KEY=Axxxxxxxxxxxxxxxxxxxxx

 

export FILESTACK_APP_SECRET=your-app-secret-here

Step 2: Build the service wrapper

Calling the SDK directly from route handlers couples your business logic to Filestack and makes testing harder. A service module isolates the integration in one place.

 

Create services/filestack_service.py:

 

# services/filestack_service.py




import time




from flask import current_app




from filestack import Client, Filelink, Security





def _security(expiry_seconds=3600, calls=None, handle=None):




    """Build a Security object scoped to specific calls and (optionally) a handle."""




    policy = {




        "expiry": int(time.time()) + expiry_seconds,




        "call": calls or ["pick", "read", "store", "remove", "convert"],




    }




    if handle:




        policy["handle"] = handle




    return Security(policy, current_app.config["FILESTACK_APP_SECRET"])





def _client(with_security=True):




    """Return a Filestack Client, optionally with a signing policy attached."""




    api_key = current_app.config["FILESTACK_API_KEY"]




    if with_security:




        return Client(api_key, security=_security())




    return Client(api_key)





def upload_filestorage(file_storage, store_params=None):




    """




    Upload a Werkzeug FileStorage (request.files['x']) to Filestack




    and return the Filelink.




    """




    client = _client(with_security=True)




    params = dict(store_params or {})




    if file_storage.filename and "filename" not in params:




        params["filename"] = file_storage.filename




    if file_storage.mimetype and "mimetype" not in params:




        params["mimetype"] = file_storage.mimetype




    return client.upload(file_obj=file_storage.stream, store_params=params)





def signed_url_for(handle, expiry_seconds=3600):




    """Return a time-limited signed CDN URL for a file handle."""




    security = _security(expiry_seconds=expiry_seconds, calls=["read", "convert"], handle=handle)




    filelink = Filelink(handle, security=security)




    return filelink.signed_url





def delete_handle(handle):




    """Delete an uploaded file by its handle."""




    api_key = current_app.config["FILESTACK_API_KEY"]




    security = _security(calls=["remove"], handle=handle)




    filelink = Filelink(handle, apikey=api_key, security=security)




    filelink.delete()





def fetch_metadata(handle, fields=None):




    """Pull metadata for an existing handle."""




    api_key = current_app.config["FILESTACK_API_KEY"]




    filelink = Filelink(handle, apikey=api_key)




    return filelink.metadata(fields or ["size", "mimetype", "filename"])

 

Two design points worth calling out. First, file_obj accepts anything with a .read() method, and FileStorage.stream is exactly that, so the upload streams straight from the request without touching disk. Second, every Security policy pins to the narrowest set of calls and (when relevant) a specific handle. That way a leaked signed URL for one file cannot be used to read another.

Step 3: Wire up an upload route

Build a blueprint that accepts a multipart form, uploads to Filestack, and persists the handle.

 

# routes/uploads.py




from flask import Blueprint, jsonify, request, abort




from flask_login import login_required, current_user




from services import filestack_service




from models import db, Document




uploads_bp = Blueprint("uploads", __name__, url_prefix="/api/uploads")





@uploads_bp.post("/")




@login_required




def create_upload():




    if "file" not in request.files:




        abort(400, description="No file part in request")




    file_storage = request.files["file"]




    if not file_storage.filename:




        abort(400, description="Empty filename")




    filelink = filestack_service.upload_filestorage(




        file_storage,




        store_params={"path": f"user-uploads/{current_user.id}/"},




    )




    doc = Document(




        owner_id=current_user.id,




        filestack_handle=filelink.handle,




        filestack_url=filelink.url,




        original_name=file_storage.filename,




        size_bytes=request.content_length or 0,




    )




    db.session.add(doc)




    db.session.commit()




    return jsonify({




        "id": doc.id,




        "handle": filelink.handle,




        "url": filelink.url,




    }), 201

 

The companion SQLAlchemy model:

 

# models.py




from datetime import datetime




from flask_sqlalchemy import SQLAlchemy




db = SQLAlchemy()





class Document(db.Model):




    __tablename__ = "documents"




    id = db.Column(db.Integer, primary_key=True)




    owner_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)




    filestack_handle = db.Column(db.String(64), unique=True, nullable=False)




    filestack_url = db.Column(db.String(500), nullable=False)




    original_name = db.Column(db.String(255), nullable=False)




    size_bytes = db.Column(db.BigInteger, default=0)




    created_at = db.Column(db.DateTime, default=datetime.utcnow)

 

Store the handle, not just the URL. The handle is the durable identifier you need for generating fresh signed URLs, chaining transformations, or deleting the file later. The URL is convenience metadata.

Step 4: Serve files with signed URLs

Public files can use filelink.url directly. For anything private, generate a signed URL on demand with a short expiry.

 

# routes/uploads.py (continued)




from flask import Blueprint, jsonify, abort




from flask_login import login_required, current_user




from services import filestack_service




from models import Document





@uploads_bp.get("/<int:doc_id>/download-url")




@login_required




def download_url(doc_id):




    doc = Document.query.get_or_404(doc_id)




    if doc.owner_id != current_user.id:




        abort(403)




    url = filestack_service.signed_url_for(doc.filestack_handle, expiry_seconds=300)




    return jsonify({"url": url})

 

The frontend hits this endpoint, gets a five-minute URL, and either redirects or fetches the file. After the expiry, the URL stops working. Leaked links go stale fast.

Step 5: Apply transformations on the fly

Filestack transformations happen at delivery time. Chain methods on a Filelink and use the resulting URL.

 

# services/filestack_service.py (continued)




def thumbnail_url(handle, width=200, height=200):




    """Build a transformation URL that resizes and crops to a thumbnail."""




    api_key = current_app.config["FILESTACK_API_KEY"]




    security = _security(calls=["read", "convert"], handle=handle)




    filelink = Filelink(handle, apikey=api_key, security=security)




    transform = filelink.resize(width=width, height=height, fit="crop").enhance()




    return transform.url




Expose it in a route, a Jinja filter, or a serializer:




# app.py (inside create_app, after blueprints are registered)




from services import filestack_service




@app.template_filter("fs_thumb")




def fs_thumb(handle, width=200, height=200):




    return filestack_service.thumbnail_url(handle, width, height)




<img src="{{ document.filestack_handle|fs_thumb(400, 400) }}" alt="{{ document.original_name }}">

 

The transformation runs on Filestack’s processing engine and the result is cached on the CDN. Your Flask app never touches the bytes.

Step 6: Build a Flask extension for cleaner reuse

If you want a more idiomatic Flask shape, wrap the SDK in an extension class that follows the standard init_app pattern. This is the closest thing to a “native Flask integration.”

 

# extensions/filestack.py




import time




from flask import current_app, g




from filestack import Client, Filelink, Security





class Filestack:




    def __init__(self, app=None):




        if app is not None:




            self.init_app(app)




    def init_app(self, app):




        app.config.setdefault("FILESTACK_API_KEY", None)




        app.config.setdefault("FILESTACK_APP_SECRET", None)




        app.extensions = getattr(app, "extensions", {})




        app.extensions["filestack"] = self




    def _security(self, calls=None, expiry_seconds=3600, handle=None):




        policy = {




            "expiry": int(time.time()) + expiry_seconds,




            "call": calls or ["pick", "read", "store", "remove", "convert"],




        }




        if handle:




            policy["handle"] = handle




        return Security(policy, current_app.config["FILESTACK_APP_SECRET"])




    @property




    def client(self):




        # Cache one client per request




        if "filestack_client" not in g:




            g.filestack_client = Client(




                current_app.config["FILESTACK_API_KEY"],




                security=self._security(),




            )




        return g.filestack_client




    def upload(self, file_storage, store_params=None):




        params = dict(store_params or {})




        if file_storage.filename and "filename" not in params:




            params["filename"] = file_storage.filename




        if file_storage.mimetype and "mimetype" not in params:




            params["mimetype"] = file_storage.mimetype




        return self.client.upload(file_obj=file_storage.stream, store_params=params)




    def signed_url(self, handle, expiry_seconds=3600):




        security = self._security(calls=["read", "convert"], handle=handle, expiry_seconds=expiry_seconds)




        return Filelink(handle, security=security).signed_url




    def delete(self, handle):




        security = self._security(calls=["remove"], handle=handle)




        Filelink(handle, apikey=current_app.config["FILESTACK_API_KEY"], security=security).delete()

 

Register it in your factory:

 

# app.py




from flask import Flask




from config import Config




from extensions.filestack import Filestack




filestack = Filestack()




def create_app():




    app = Flask(__name__)




    app.config.from_object(Config)




    filestack.init_app(app)




    from routes.uploads import uploads_bp




    app.register_blueprint(uploads_bp)




    return app

 

Use it from any view:

 

from app import filestack

 

@uploads_bp.post(“/v2”)

 

def create_upload_v2():

 

    filelink = filestack.upload(request.files[“file”])

 

    return {“handle”: filelink.handle, “url”: filelink.url}

 

The g cache means each request reuses one Client, which keeps connection setup overhead down.

Step 7: Verify Filestack webhooks

When you configure webhooks for events like upload completion or workflow finish, Filestack signs every request. Your Flask route has to verify the signature against the raw body before trusting the payload.

 

# routes/webhooks.py




import json




from flask import Blueprint, request, current_app, abort, jsonify




from filestack.helpers import verify_webhook_signature




webhooks_bp = Blueprint("webhooks", __name__, url_prefix="/webhooks")





@webhooks_bp.post("/filestack")




def filestack_webhook():




    raw_body = request.get_data()  # raw bytes, must not be parsed first




    headers = {




        "FS-Signature": request.headers.get("FS-Signature", ""),




        "FS-Timestamp": request.headers.get("FS-Timestamp", ""),




    }




    valid, details = verify_webhook_signature(




        current_app.config["FILESTACK_WEBHOOK_SECRET"],




        raw_body,




        headers,




    )




    if not valid:




        abort(400, description=f"Invalid signature: {details.get('error')}")




    payload = json.loads(raw_body)




    action = payload.get("action")




    # Branch on action type: fp.upload, fp.video_complete, fp.workflow, etc.




    # Your business logic goes here.




    return jsonify({"received": True})

 

Two requirements that catch people out. First, pass request.get_data() (raw bytes) to verify_webhook_signature, not a parsed dict. Reading request.json first will consume the stream and break signature verification. Second, if you use Flask-WTF or any global CSRF protection, exempt this route. Filestack is not your frontend and has no CSRF token to send.

 

If you use Flask-WTF, the exemption looks like this:

 

from flask_wtf.csrf import CSRFProtect




csrf = CSRFProtect()




# inside create_app, after registering the blueprint




csrf.exempt(webhooks_bp)

Step 8: Pair with the JavaScript Picker for client-side uploads

The Python SDK is the right tool for server-side work: webhook handling, background jobs, batch processing, admin tooling. For user-facing uploads from the browser, the Filestack JavaScript Picker is faster because the file goes from the browser straight to Filestack, skipping your Flask app entirely.

 

The pattern looks like this:

 

  1. Frontend uses the JS Picker to upload directly to Filestack
  2. Picker returns a handle to your frontend
  3. Frontend POSTs the handle to a Flask endpoint
  4. Flask validates, saves the handle to your model, and runs any server-side logic

 

# routes/uploads.py (continued)




@uploads_bp.post("/register")




@login_required




def register_handle():




    handle = request.json.get("handle")




    if not handle:




        abort(400, description="handle required")




    metadata = filestack_service.fetch_metadata(handle, fields=["size", "mimetype", "filename"])




    doc = Document(




        owner_id=current_user.id,




        filestack_handle=handle,




        filestack_url=f"https://cdn.filestackcontent.com/{handle}",




        original_name=metadata.get("filename", ""),




        size_bytes=metadata.get("size", 0),




    )




    db.session.add(doc)




    db.session.commit()




    return jsonify({"id": doc.id}), 201

This keeps large file transfers off your Flask servers and uses the Python SDK where it earns its keep: validation, persistence, and downstream processing.

Putting it all together

You now have a working pattern: SDK installed, service wrapper isolating Filestack calls, blueprints handling uploads and downloads, signed URLs for private files, transformations chained for delivery, an optional Flask extension for a more idiomatic shape, and verified webhooks for async events. Every snippet uses real methods from filestack-python and runs as written.

 

Flask has no Filestack-specific extension because it does not need one. Werkzeug’s FileStorage is already a file-like object, and the Python SDK takes file-like objects directly. Build the wrapper once and the rest of your Flask app talks to a normal Python module.

 

Start building faster with Filestack. Get your free API key and add uploads, signed downloads, transformations, and webhook support to your Flask app.

 

Exit mobile version