File Upload API: How to Upload Files with cURL, Python & JavaScript
A file upload API is a REST endpoint that accepts a file over HTTP and returns a permanent URL you can share or store. This guide covers how file upload APIs work under the hood (multipart/form-data, presigned URLs, REST design patterns) and gives you working code in cURL, Python, and JavaScript — both browser and Node.js — that you can copy straight into your project.
What Is a File Upload API?
A file upload API is a web service that accepts files over HTTP and stores them for you. Instead of building your own upload server, configuring storage buckets, setting up CDNs, and managing disk space, you send a POST request with your file and get back a URL where that file is permanently hosted.
The typical flow looks like this:
- Your application sends an HTTP POST request with the file as
multipart/form-data - The API stores the file and returns a JSON response
- The response includes a CDN URL you can use immediately
This pattern is used everywhere: profile picture uploads, document attachments, media storage for CMS platforms, receipt uploads in fintech apps, and automated file processing pipelines. If your app handles files, you need an upload mechanism, and an API is the most portable way to build one.
Anatomy of a File Upload HTTP Request
Before looking at the code, it helps to see what actually goes over the wire. A file upload is just an HTTP POST with a specific body format called multipart/form-data. Here is what the raw request looks like when you upload a single PNG:
POST /v1/upload HTTP/1.1
Host: filepost.dev
X-API-Key: fh_abc123...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 84321
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="photo.png"
Content-Type: image/png
<binary file bytes here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
The important pieces:
- Boundary: a random string in the
Content-Typeheader that marks where each "part" of the body starts and ends. The same boundary appears in the body, prefixed with--, and a final closing marker suffixed with--. - Content-Disposition: tells the server this part is a form field called
file, and gives the original filename. - Per-part Content-Type: the MIME type of the file itself (not the request). This is what most upload APIs use to decide how to serve the file back later.
- Binary body: the raw file bytes, unencoded. No base64, no URL-encoding.
You almost never write this by hand. Every HTTP library (cURL, Python requests, browser FormData, Node.js fetch) has a helper that generates the boundary and formats the body for you. The single most common upload bug is setting Content-Type manually and breaking the boundary. More on that in the best-practices section below.
Prerequisites
To follow along with the code examples in this guide, you need:
- An API key from a file hosting service. We will use FilePost in these examples because the free tier gives you 300 uploads per month with no credit card required.
- A file to upload, any image, PDF, or document will work.
- cURL (pre-installed on macOS and most Linux distros), Python 3 with the
requestslibrary, or Node.js 18+ (for built-in fetch).
To get your FilePost API key, sign up with a single request:
curl -X POST https://filepost.dev/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
You will receive your API key in the response. Save it somewhere safe, you will need it for every upload.
Uploading Files with cURL
cURL is the fastest way to test a file upload API. The -F flag handles multipart encoding automatically, so you do not need to think about content types or boundaries.
curl -X POST https://filepost.dev/v1/upload \
-H "X-API-Key: your_api_key_here" \
-F "file=@/path/to/photo.png"
That is the entire request. The @ symbol tells cURL to read the file from disk rather than sending the literal string. cURL sets the Content-Type header to multipart/form-data automatically when you use -F.
A successful response looks like this:
{
"url": "https://cdn.filepost.dev/file/filepost/uploads/a1/a1b2c3.png",
"file_id": "a1b2c3d4e5f6",
"size": 84210
}
The url field is a permanent CDN link. You can paste it into a browser, embed it in HTML, or store it in your database. The file_id is useful for listing or deleting the file later. The size is the file size in bytes.
Uploading Multiple Files
If you need to upload several files in a batch, you can loop in bash:
for file in ./uploads/*; do
curl -s -X POST https://filepost.dev/v1/upload \
-H "X-API-Key: your_api_key_here" \
-F "file=@$file"
echo ""
done
Uploading Files with Python
Python's requests library makes file uploads straightforward. The files parameter handles multipart encoding, and the library reads the file from disk for you.
import requests
api_key = "your_api_key_here"
file_path = "/path/to/photo.png"
with open(file_path, "rb") as f:
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": api_key},
files={"file": f}
)
data = response.json()
print(f"URL: {data['url']}")
print(f"File ID: {data['file_id']}")
print(f"Size: {data['size']} bytes")
A few things to note here:
- Always open files in binary mode (
"rb"). Text mode will corrupt binary files like images and PDFs. - The
filesparameter tells requests to encode the body asmultipart/form-data. Do not set theContent-Typeheader yourself, requests needs to generate the multipart boundary automatically. - The
withstatement ensures the file handle is closed after the upload, even if an exception occurs.
Uploading In-Memory Files
Sometimes your file does not exist on disk. Maybe you generated a PDF, downloaded something from another API, or captured a screenshot. You can upload bytes directly:
import requests
from io import BytesIO
# Suppose you have raw bytes from somewhere
image_bytes = b"\x89PNG\r\n..." # your actual bytes
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": "your_api_key_here"},
files={"file": ("generated.png", BytesIO(image_bytes), "image/png")}
)
print(response.json()["url"])
The tuple format (filename, file_object, content_type) gives you full control over the upload metadata without needing a file on disk.
Uploading Files from JavaScript (Browser and Node.js)
JavaScript has two very different upload contexts: a web page where the file comes from an <input type="file"> element, and a Node.js script reading from disk. The HTTP request is identical, but the way you get the file bytes differs.
Browser: HTML Form and fetch
In a browser, the user picks a file through a file input. You grab the File object from input.files[0], wrap it in a FormData, and POST it with fetch. No libraries needed.
<input type="file" id="fileInput">
<button id="uploadBtn">Upload</button>
<script>
document.getElementById("uploadBtn").addEventListener("click", async () => {
const input = document.getElementById("fileInput");
const file = input.files[0];
if (!file) return;
const form = new FormData();
form.append("file", file);
const response = await fetch("https://filepost.dev/v1/upload", {
method: "POST",
headers: { "X-API-Key": "your_api_key_here" },
body: form,
});
const data = await response.json();
console.log("Uploaded to:", data.url);
});
</script>
A few important points for browser uploads:
- Never hardcode your API key in client-side JavaScript. Anyone can open DevTools and copy it. For browser uploads in production, either proxy the request through your own backend, or use a short-lived intake link or signed upload URL instead.
- Do not set the
Content-Typeheader.fetchgenerates the correct multipart boundary automatically when you pass aFormDatabody. - To show upload progress, use
XMLHttpRequestinstead offetch.fetchdoes not expose upload progress events as of the current spec.
Node.js: readFile and fetch
Node.js 18 and later ship with a built-in fetch API and FormData support, so you do not need any third-party packages. However, reading a file from disk still requires the fs module.
import { readFile } from "fs/promises";
const apiKey = "your_api_key_here";
const filePath = "/path/to/photo.png";
const fileBuffer = await readFile(filePath);
const file = new File([fileBuffer], "photo.png", { type: "image/png" });
const form = new FormData();
form.append("file", file);
const response = await fetch("https://filepost.dev/v1/upload", {
method: "POST",
headers: { "X-API-Key": apiKey },
body: form,
});
const data = await response.json();
console.log(`URL: ${data.url}`);
console.log(`File ID: ${data.file_id}`);
console.log(`Size: ${data.size} bytes`);
Key details for Node.js uploads:
- Do not set the
Content-Typeheader manually. ThefetchAPI generates the correct multipart boundary when you pass aFormDatabody. - The
Fileconstructor takes an array of data chunks, a filename, and an options object with the MIME type. - This example uses top-level
await, which works in ES modules (set"type": "module"in yourpackage.json).
Handling API Responses
A well-built upload client handles both success and failure cases. Here is a more robust Python example with error handling:
import requests
def upload_file(file_path, api_key):
try:
with open(file_path, "rb") as f:
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": api_key},
files={"file": f},
timeout=30
)
if response.status_code == 200:
data = response.json()
return data["url"]
elif response.status_code == 401:
raise Exception("Invalid API key")
elif response.status_code == 413:
raise Exception("File too large for your plan")
elif response.status_code == 429:
raise Exception("Upload limit reached for this month")
else:
raise Exception(f"Upload failed: {response.status_code}")
except requests.exceptions.Timeout:
raise Exception("Upload timed out, check your connection")
except FileNotFoundError:
raise Exception(f"File not found: {file_path}")
The most common HTTP status codes you will encounter with file upload APIs:
- 200, Upload succeeded. Parse the JSON body for the URL.
- 400, Bad request. Usually means the
filefield is missing or the body is not multipart-encoded. - 401, Authentication failed. Check your API key.
- 413, File exceeds the maximum size for your plan.
- 429, Rate limit hit. You have used all your uploads for the current billing period.
Start Uploading Files in 30 Seconds
FilePost gives you 300 free uploads per month. No credit card, no configuration, no storage management.
Get Your Free API KeyBest Practices for API File Uploads
After building dozens of integrations that handle file uploads, here are the practices that prevent headaches:
1. Always Set a Timeout
File uploads can hang indefinitely if the network connection drops mid-transfer. Set a timeout proportional to the expected file size. For most uploads under 50MB, 30 seconds is a reasonable starting point.
2. Validate Before Uploading
Check file size and type on the client side before sending the request. Uploading a 500MB file only to get a 413 response wastes time and bandwidth. If your API has a 200MB limit, reject anything larger before the upload starts.
3. Use Streaming for Large Files
If you are uploading files larger than a few hundred megabytes, avoid reading the entire file into memory. Most HTTP libraries support streaming uploads that read the file in chunks. In Python, requests streams file uploads by default when you pass a file object to the files parameter.
4. Store the File ID, Not Just the URL
The URL is what you will use most of the time, but the file ID is essential for management operations like listing and deleting files. Store both in your database.
5. Handle Retries Carefully
File uploads are not idempotent, retrying a failed upload may create a duplicate. If you need retry logic, implement it at the application level with deduplication checks rather than using generic HTTP retry middleware.
6. Do Not Set Content-Type Manually
This is the single most common mistake. When sending multipart/form-data, the Content-Type header includes a boundary string that separates the parts. If you set the header yourself, the boundary will not match the body, and the server will reject the request. Let your HTTP library set it automatically.
REST API Design Patterns for File Uploads
When you design or choose a file upload API, there are three common patterns. Each has tradeoffs around performance, security, and implementation complexity.
Pattern 1: Direct multipart/form-data upload
The client POSTs the file directly to the API server, which handles storage itself. This is what the examples above use. It is the simplest pattern: one request, one response, no coordination between the client and a storage backend.
Good for: small-to-medium files (under 100 MB), APIs that need to inspect or transform files during upload, simple integrations where one HTTP call is easier than two.
Bad for: very large files (the API server has to proxy every byte), high-throughput systems (upload traffic competes with regular API traffic on the same infrastructure).
Pattern 2: Base64-encoded JSON
The client reads the file, encodes it as base64, and sends it as a JSON field:
{
"filename": "photo.png",
"content_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
Good for: schemas that are already JSON end-to-end, environments where you cannot easily send multipart (some legacy SOAP-to-REST bridges, certain embedded systems).
Bad for: almost everything else. Base64 adds ~33% overhead, the entire file sits in memory as a string on both client and server, and JSON parsers have to process every byte. Avoid this pattern unless you have a specific reason.
Pattern 3: Presigned URLs (two-step upload)
The client makes an API call asking "where should I upload this?", the API returns a short-lived presigned URL pointing directly at object storage (S3, B2, GCS), and the client PUTs the file to that URL. The API server never handles the file bytes.
// Step 1: ask the API for an upload URL
POST /v1/uploads/presign
→ { "upload_url": "https://...signed...", "file_id": "abc123" }
// Step 2: PUT the file directly to the presigned URL
PUT <upload_url>
Content-Type: image/png
<binary bytes>
Good for: large files, high-throughput systems, browser uploads where you do not want to proxy through your own servers, any case where you want upload traffic to go straight to storage.
Bad for: APIs that need to validate or process files during upload (you can still do it after, via a webhook or storage event), simple use cases where the extra round trip is not worth the complexity.
For most applications, direct multipart is the right default. Switch to presigned URLs when files get large (above ~100 MB) or when upload volume becomes significant.
Popular File Upload APIs Compared
If you are evaluating options, here is a quick comparison of the file upload APIs developers most commonly consider in 2026:
| API | Free tier | Max file size (paid) | Pricing model | Best for |
|---|---|---|---|---|
| FilePost | 300 uploads/mo | 500 MB | Flat monthly ($9 / $29) | Predictable billing, simple REST API, permanent URLs |
| file.io | 2 GB/file (ephemeral) | Up to 5 GB | Per-download | One-time file transfers, self-destructing links |
| UploadThing | 2 GB storage | Varies by plan | Storage + bandwidth tiers | Next.js apps, React component integration |
| Cloudinary | 25 GB storage | Varies by media type | Credits (storage + transforms) | Image/video transformation pipelines |
| AWS S3 + presigned URLs | 5 GB (12 months) | 5 TB | Per-GB storage + egress | Very large files, custom pipelines, enterprise scale |
See the UploadThing alternative, file.io alternative, and FilePost vs Cloudinary comparisons for deeper breakdowns of each.
Getting Started with FilePost
If you are looking for a file upload API that just works, FilePost is built for exactly this use case. There is no bucket configuration, no IAM policies, no region selection, and no SDK to install. You make one HTTP request and get back a CDN URL.
Here is what you get:
- Free tier: 300 uploads per month, 50MB max file size, 2GB storage, unlimited bandwidth
- Starter ($9/mo): 5,000 uploads per month, 200MB max file size
- Pro ($29/mo): 25,000 uploads per month, 500MB max file size
Paid plans include unlimited storage, unlimited bandwidth, CDN delivery, and files that live forever. The free tier includes 2GB storage. There are no per-GB storage fees or egress charges. If you want to compare FilePost with other options, see our FilePost vs Cloudinary and uploadtourl alternative comparisons.
You can also manage your files with the API. List all your uploaded files:
curl https://filepost.dev/v1/files \
-H "X-API-Key: your_api_key_here"
Delete a file by ID:
curl -X DELETE https://filepost.dev/v1/files/a1b2c3d4e5f6 \
-H "X-API-Key: your_api_key_here"
The full API documentation includes interactive Swagger docs where you can test every endpoint directly in your browser.