How to Upload Files from Python (No SDK Required)
Step-by-step guide to uploading files from Python via the files.link REST API — three HTTP calls, no vendor SDK, works with requests / httpx / urllib3.
Why no SDK?
Most cloud storage providers push you toward a vendor SDK: boto3 for AWS, google-cloud-storage for GCP, cloudinary for Cloudinary. Each one adds megabytes to your Python environment, locks you to a specific auth flow, and ships its own retry / pagination / error-handling abstractions you have to learn on top of HTTP.
files.link is a REST API. Three HTTP calls give you an upload + a CDN URL. The standard requests library covers it; httpx if you want async; urllib3 if you want zero dependencies. Your choice.
The 3-step flow
files.link uses presigned URLs for the actual file transfer — a security pattern that keeps your API key off the network during the bulk upload and lets the file go directly to object storage without proxying through our backend.
1. POST file metadata to /v1/files/{folderId} with your API key. Response includes a presigned PUT URL.
2. PUT the file bytes to the presigned URL (no auth header needed — the URL itself carries a short-lived signature).
3. POST a confirmation to /v1/files/confirm-upload with the file id. This flips the row to uploaded: true so it becomes visible to listings.
Yes, that's three calls instead of one. It's a deliberate trade — the actual file bytes never touch our HTTP servers, so uploads scale linearly with object storage capacity instead of our backend's network bandwidth.
Code: minimal Python upload
Here's the whole flow in ~20 lines using requests:
import os
import requests
API_KEY = os.environ["FILESLINK_KEY"]
FOLDER_ID = "your-folder-uuid-here"
BASE = "https://api.files.link"
def upload(path: str) -> str:
"""Upload a file and return its public CDN URL."""
filename = os.path.basename(path)
size = os.path.getsize(path)
# Step 1: ask for a presigned URL
meta_res = requests.post(
f"{BASE}/v1/files/{FOLDER_ID}",
headers={"Authorization": API_KEY},
json={"filesMetadata": [{"name": filename, "size": size}]},
)
meta_res.raise_for_status()
entry = meta_res.json()["urls"][0]
# Step 2: PUT the file bytes
with open(path, "rb") as f:
put_res = requests.put(entry["url"], data=f)
put_res.raise_for_status()
# Step 3: confirm so the file becomes visible
confirm_res = requests.post(
f"{BASE}/v1/files/confirm-upload",
headers={"Authorization": API_KEY},
json={"ids": [entry["id"]]},
)
confirm_res.raise_for_status()
return entry["s3Key"] # use this to build your CDN URLDrop your API key into FILESLINK_KEY, set FOLDER_ID to a folder you created in the dashboard, and call upload("/path/to/file.pdf").
Adding retries and error handling
The above works for the happy path. Real code needs retries on the PUT step (presigned URLs are time-limited; networks fail).
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["POST", "PUT"],
)
session.mount("https://", HTTPAdapter(max_retries=retries))
# Then use session.post() and session.put() instead of requests.post() / requests.put()For files larger than ~100MB, switch to the multipart endpoint (/v1/files/multipart/{folderId}/initiate). The flow is similar but you PUT each part to its own presigned URL — easy to parallelize with concurrent.futures.
When this beats the SDK approach
If you're already running a Python service with HTTP clients in the stack (requests, httpx, urllib3, aiohttp), files.link costs zero new dependencies. The 3-call presigned flow is verbose, but it's also self-documenting — you can read the request in network logs and immediately know what's happening.
If you're starting a project where you want a managed CDN + storage + simple billing, Python file upload via files.link is a 20-line solution. If you're already deep in boto3 and have IAM policies + CloudFront set up, the AWS SDK is the right call.