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
- Filestack has no Flask-specific package, but filestack-python works inside any Flask view because Werkzeug’s FileStorage is a file-like object
- The file_obj parameter on client.upload() accepts request.files[‘file’] directly, so the bytes never hit your disk
- A small service module keeps Filestack calls out of your route handlers and makes testing straightforward
- Security policies sign every download URL with an expiry, which is how you serve private files from a public CDN
- Webhook verification needs the raw request body, not parsed JSON, and the route must skip CSRF protection
Before you start
You need:
- Python 3.7 or higher
- Flask 2.x or 3.x
- A Filestack account for your API key and app secret
- Familiarity with Flask blueprints, request handling, and application factories
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:
- Frontend uses the JS Picker to upload directly to Filestack
- Picker returns a handle to your frontend
- Frontend POSTs the handle to a Flask endpoint
- 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.
A Product Marketing Manager at Filestack with four years of dedicated experience. As a true technology enthusiast, they pair marketing expertise with a deep technical background. This allows them to effectively translate complex product capabilities into clear value for a developer-focused audience.
Read More →

