FilePost API Documentation
Follow one file from upload to deletion, then use the same verified requests in cURL, JavaScript, Python, or PHP.
Quickstart
FilePost turns a file into a public CDN URL. This guide follows one realistic file through its complete lifecycle so you can see how the endpoints fit together.
An invoice sent from your application
Your backend generates invoice-2026-1048.pdf, uploads it to
FilePost, and places the returned URL in a customer email. You then inspect
it, choose whether its name appears in the URL, list it, and delete it when
your retention period ends.
- File
- invoice-2026-1048.pdf
- Content type
- application/pdf
- Visibility
- public
Before running an example, save your key in an environment variable:
export FILEPOST_API_KEY="fh_your_api_key"// macOS/Linux:
// export FILEPOST_API_KEY="fh_your_api_key"
//
// Windows PowerShell:
// $env:FILEPOST_API_KEY="fh_your_api_key"# macOS/Linux:
# export FILEPOST_API_KEY="fh_your_api_key"
#
# Windows PowerShell:
# $env:FILEPOST_API_KEY="fh_your_api_key"// macOS/Linux:
// export FILEPOST_API_KEY="fh_your_api_key"
//
// Windows PowerShell:
// $env:FILEPOST_API_KEY="fh_your_api_key"Keep API keys on your server. Do not embed them in browser JavaScript, public repositories, mobile applications, or frontend bundles.
Authentication
Every authenticated request sends the API key in this header:
X-API-Key: fh_your_api_key
Upload and account URLs are public only after FilePost returns them. API management endpoints still require your key. A file URL can be opened by anyone who has it, regardless of whether its filename is visible in that URL.
Base URL
Use this base URL for normal API requests:
https://filepost.dev
Uploads go to https://upload.filepost.dev/v1/upload for every plan and file
size up to your plan limit (500 MB on Pro). Uploaded files are served from
https://cdn.filepost.dev.
Files
Use the file endpoints to upload, inspect, list, change filename visibility,
and delete files. The stable file_id returned at upload is used for later
management calls.
Step 1 of 6 Upload the invoice
POST/v1/uploadUpload a file
Send a multipart/form-data request. The file part supplies the stored and
downloaded filename.
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | yes | File bytes and filename. |
expires_in |
string | no | Auto-delete period such as 30s, 15m, 24h, or 7d. |
filename_mode |
string | no | download keeps the name out of the URL. public includes it. The account default is used when omitted. |
The invoice is safe to show in the URL because its name does not contain a
customer name or other private information. We therefore choose public.
curl -X POST https://upload.filepost.dev/v1/upload \
-H "X-API-Key: $FILEPOST_API_KEY" \
-F "file=@invoice-2026-1048.pdf;type=application/pdf" \
-F "filename_mode=public" \
-F "expires_in=30d"import { readFile } from "node:fs/promises";
const bytes = await readFile("invoice-2026-1048.pdf");
const form = new FormData();
form.append(
"file",
new Blob([bytes], { type: "application/pdf" }),
"invoice-2026-1048.pdf"
);
form.append("filename_mode", "public");
form.append("expires_in", "30d");
const response = await fetch("https://upload.filepost.dev/v1/upload", {
method: "POST",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY },
body: form
});
if (!response.ok) throw new Error(await response.text());
const uploaded = await response.json();
console.log(uploaded.url);import os
import requests
with open("invoice-2026-1048.pdf", "rb") as invoice:
response = requests.post(
"https://upload.filepost.dev/v1/upload",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
files={"file": (
"invoice-2026-1048.pdf",
invoice,
"application/pdf",
)},
data={"filename_mode": "public", "expires_in": "30d"},
timeout=60,
)
response.raise_for_status()
uploaded = response.json()
print(uploaded["url"])<?php
$file = new CURLFile(
"invoice-2026-1048.pdf",
"application/pdf",
"invoice-2026-1048.pdf"
);
$ch = curl_init("https://upload.filepost.dev/v1/upload");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_POSTFIELDS => [
"file" => $file,
"filename_mode" => "public",
"expires_in" => "30d",
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
$uploaded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $uploaded["url"];Response
{
"file_id": "a84f2c6d921a4f53a1e7c8d9b04e6210",
"url": "https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210/invoice-2026-1048.pdf",
"name": "invoice-2026-1048.pdf",
"size": 248193,
"content_type": "application/pdf",
"expires_at": "2026-08-28T10:30:00",
"filename_mode": "public"
}
Save both file_id and url. The URL is what you send to the customer. The
ID is what your application uses for later management calls.
Once a verified free account has used 60% of its monthly allowance, successful
multipart and Base64 uploads also include an additive quota object:
{
"quota": {
"code": "monthly_upload_limit_approaching",
"message": "You have 20 monthly uploads remaining. Upgrade before automated uploads begin returning HTTP 403.",
"used": 30,
"limit": 50,
"remaining": 20,
"reset_at": "2026-09-01T00:00:00Z",
"upgrade": {
"plan": "starter",
"url": "https://filepost.dev/#pricing",
"promotion_code": "PEERPUSH",
"offer": "30% off Starter for the first 3 months",
"uploads_per_month": 1500
}
}
}
The upload still succeeded. Treat this object as an early warning and keep
processing the returned file URL. On the final successful slot, code becomes
monthly_upload_limit_reached, remaining is 0, and subsequent uploads
return HTTP 403 until the allowance resets or the account upgrades.
POST/v1/upload/base64Upload Base64 data
Use this endpoint when your automation or application produces Base64 text instead of a multipart file. The URL and headers stay the same as other API calls. Only the JSON body differs.
| Field | Type | Required | Description |
|---|---|---|---|
filename |
string | yes | Stored and downloaded filename, including its extension. |
content_type |
string | no | MIME type. Defaults to application/octet-stream. |
data_base64 |
string | yes | Base64 file bytes. file_base64 and data are accepted aliases. |
expires_in |
string | no | Auto-delete period such as 24h or 30d. |
filename_mode |
string | no | download or public. |
BASE64_DATA=$(base64 < invoice-2026-1048.pdf | tr -d '\n')
curl -X POST https://upload.filepost.dev/v1/upload/base64 \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"invoice-2026-1048.pdf\",
\"content_type\": \"application/pdf\",
\"data_base64\": \"$BASE64_DATA\",
\"expires_in\": \"30d\",
\"filename_mode\": \"public\"
}"import { readFile } from "node:fs/promises";
const bytes = await readFile("invoice-2026-1048.pdf");
const response = await fetch("https://upload.filepost.dev/v1/upload/base64", {
method: "POST",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
filename: "invoice-2026-1048.pdf",
content_type: "application/pdf",
data_base64: bytes.toString("base64"),
expires_in: "30d",
filename_mode: "public"
})
});
if (!response.ok) throw new Error(await response.text());
const uploaded = await response.json();
console.log(uploaded.url);import base64
import os
import requests
with open("invoice-2026-1048.pdf", "rb") as invoice:
encoded = base64.b64encode(invoice.read()).decode("ascii")
response = requests.post(
"https://upload.filepost.dev/v1/upload/base64",
headers={
"X-API-Key": os.environ["FILEPOST_API_KEY"],
"Content-Type": "application/json",
},
json={
"filename": "invoice-2026-1048.pdf",
"content_type": "application/pdf",
"data_base64": encoded,
"expires_in": "30d",
"filename_mode": "public",
},
timeout=60,
)
response.raise_for_status()
uploaded = response.json()
print(uploaded["url"])<?php
$payload = [
"filename" => "invoice-2026-1048.pdf",
"content_type" => "application/pdf",
"data_base64" => base64_encode(
file_get_contents("invoice-2026-1048.pdf")
),
"expires_in" => "30d",
"filename_mode" => "public",
];
$ch = curl_init("https://upload.filepost.dev/v1/upload/base64");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
$uploaded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $uploaded["url"];The response is identical to the multipart upload response.
Step 2 of 6 Put the returned URL in the customer email
The url is already a public HTTPS URL. Your email system can use it directly:
Your invoice is ready:
https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210/invoice-2026-1048.pdf
No FilePost authentication is required when the customer opens the file URL. Do not send your API key to the customer.
Step 3 of 6 Inspect the stored file
GET/v1/files/{file_id}Get file metadata
Use the stable file_id from the upload response.
curl https://filepost.dev/v1/files/a84f2c6d921a4f53a1e7c8d9b04e6210 \
-H "X-API-Key: $FILEPOST_API_KEY"const fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
const response = await fetch(
`https://filepost.dev/v1/files/${fileId}`,
{ headers: { "X-API-Key": process.env.FILEPOST_API_KEY } }
);
if (!response.ok) throw new Error(await response.text());
const file = await response.json();
console.log(file);import os
import requests
file_id = "a84f2c6d921a4f53a1e7c8d9b04e6210"
response = requests.get(
f"https://filepost.dev/v1/files/{file_id}",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
$ch = curl_init("https://filepost.dev/v1/files/" . $fileId);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));Response
{
"file_id": "a84f2c6d921a4f53a1e7c8d9b04e6210",
"url": "https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210/invoice-2026-1048.pdf",
"name": "invoice-2026-1048.pdf",
"size": 248193,
"content_type": "application/pdf",
"created_at": "2026-07-29T10:30:00",
"expires_at": "2026-08-28T10:30:00",
"filename_mode": "public"
}
Step 4 of 6 Choose whether the filename appears in the URL
PATCH/v1/files/{file_id}Change filename visibility
This changes only the canonical URL:
publicincludes the existing filename in the URL.downloadkeeps the filename out of the URL while downloads still use the correct filename.
It does not rename or re-upload the file. The bytes, file_id, expiration,
download filename, and upload usage remain unchanged.
curl -X PATCH \
https://filepost.dev/v1/files/a84f2c6d921a4f53a1e7c8d9b04e6210 \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename_mode":"download"}'const fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
const response = await fetch(
`https://filepost.dev/v1/files/${fileId}`,
{
method: "PATCH",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ filename_mode: "download" })
}
);
if (!response.ok) throw new Error(await response.text());
const updated = await response.json();
console.log(updated.url);import os
import requests
file_id = "a84f2c6d921a4f53a1e7c8d9b04e6210"
response = requests.patch(
f"https://filepost.dev/v1/files/{file_id}",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
json={"filename_mode": "download"},
timeout=30,
)
response.raise_for_status()
print(response.json()["url"])<?php
$fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
$ch = curl_init("https://filepost.dev/v1/files/" . $fileId);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(
["filename_mode" => "download"],
JSON_THROW_ON_ERROR
),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
$updated = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $updated["url"];Response
{
"file_id": "a84f2c6d921a4f53a1e7c8d9b04e6210",
"url": "https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210",
"name": "invoice-2026-1048.pdf",
"size": 248193,
"content_type": "application/pdf",
"created_at": "2026-07-29T10:30:00",
"expires_at": "2026-08-28T10:30:00",
"filename_mode": "download",
"changed": true,
"previous_url": "https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210/invoice-2026-1048.pdf",
"previous_url_preserved": false
}
Hiding the filename revokes the previous filename-visible URL. Showing it preserves the previous opaque URL so existing integrations continue to work. Browser history, logs, analytics, and copies outside FilePost cannot be erased.
Step 5 of 6 Find the invoice in your file list
GET/v1/filesList files
Results are newest first. page starts at 1 and per_page can be between 1
and 100.
curl "https://filepost.dev/v1/files?page=1&per_page=50" \
-H "X-API-Key: $FILEPOST_API_KEY"const query = new URLSearchParams({ page: "1", per_page: "50" });
const response = await fetch(
`https://filepost.dev/v1/files?${query}`,
{ headers: { "X-API-Key": process.env.FILEPOST_API_KEY } }
);
if (!response.ok) throw new Error(await response.text());
const page = await response.json();
console.log(page.files);import os
import requests
response = requests.get(
"https://filepost.dev/v1/files",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
params={"page": 1, "per_page": 50},
timeout=30,
)
response.raise_for_status()
print(response.json()["files"])<?php
$query = http_build_query(["page" => 1, "per_page" => 50]);
$ch = curl_init("https://filepost.dev/v1/files?" . $query);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
$page = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
print_r($page["files"]);Response
{
"total": 1,
"page": 1,
"per_page": 50,
"files": [
{
"file_id": "a84f2c6d921a4f53a1e7c8d9b04e6210",
"url": "https://cdn.filepost.dev/file/filepost/uploads/a8/a84f2c6d921a4f53a1e7c8d9b04e6210",
"name": "invoice-2026-1048.pdf",
"size": 248193,
"content_type": "application/pdf",
"created_at": "2026-07-29T10:30:00",
"expires_at": "2026-08-28T10:30:00",
"filename_mode": "download"
}
]
}
Step 6 of 6 Delete the invoice when retention ends
DELETE/v1/files/{file_id}Delete a file
Deletion removes the database record and stored object. The public URL stops working. This action cannot be undone.
curl -X DELETE \
https://filepost.dev/v1/files/a84f2c6d921a4f53a1e7c8d9b04e6210 \
-H "X-API-Key: $FILEPOST_API_KEY"const fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
const response = await fetch(
`https://filepost.dev/v1/files/${fileId}`,
{
method: "DELETE",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
file_id = "a84f2c6d921a4f53a1e7c8d9b04e6210"
response = requests.delete(
f"https://filepost.dev/v1/files/{file_id}",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$fileId = "a84f2c6d921a4f53a1e7c8d9b04e6210";
$ch = curl_init("https://filepost.dev/v1/files/" . $fileId);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));Response
{
"deleted": true,
"file_id": "a84f2c6d921a4f53a1e7c8d9b04e6210"
}
The invoice lifecycle is complete. In a production workflow, store file_id,
url, and expires_at alongside your own invoice record.
POST/v1/files/bulk-deleteDelete multiple files
Starter and Pro accounts can delete up to 100 files per request.
curl -X POST https://filepost.dev/v1/files/bulk-delete \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"file_ids":["a84f2c6d921a4f53a1e7c8d9b04e6210","b10e7fd581ef4c5c9822868358684b63"]}'const response = await fetch(
"https://filepost.dev/v1/files/bulk-delete",
{
method: "POST",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
file_ids: [
"a84f2c6d921a4f53a1e7c8d9b04e6210",
"b10e7fd581ef4c5c9822868358684b63"
]
})
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.post(
"https://filepost.dev/v1/files/bulk-delete",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
json={"file_ids": [
"a84f2c6d921a4f53a1e7c8d9b04e6210",
"b10e7fd581ef4c5c9822868358684b63",
]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$payload = ["file_ids" => [
"a84f2c6d921a4f53a1e7c8d9b04e6210",
"b10e7fd581ef4c5c9822868358684b63",
]];
$ch = curl_init("https://filepost.dev/v1/files/bulk-delete");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));Account
POST/v1/signupCreate an account
Create a free account. New email addresses receive an API key. If the email already belongs to an account, FilePost sends a secure login link instead of revealing its key.
curl -X POST https://filepost.dev/v1/signup \
-H "Content-Type: application/json" \
-d '{"email":"developer@example.com"}'const response = await fetch("https://filepost.dev/v1/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "developer@example.com" })
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import requests
response = requests.post(
"https://filepost.dev/v1/signup",
json={"email": "developer@example.com"},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/signup");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(
["email" => "developer@example.com"],
JSON_THROW_ON_ERROR
),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));GET/v1/accountRead account usage
Returns the plan, email verification state, default filename mode, monthly usage, and current limits.
curl https://filepost.dev/v1/account \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/account", {
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.get(
"https://filepost.dev/v1/account",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/account");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));POST/v1/account/filename-modeSet the upload default
This changes the default for future uploads. A filename_mode supplied during
an upload overrides the account default for that upload only.
curl -X POST https://filepost.dev/v1/account/filename-mode \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename_mode":"download"}'const response = await fetch(
"https://filepost.dev/v1/account/filename-mode",
{
method: "POST",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ filename_mode: "download" })
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.post(
"https://filepost.dev/v1/account/filename-mode",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
json={"filename_mode": "download"},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/account/filename-mode");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => '{"filename_mode":"download"}',
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));POST/v1/rotate-keyRotate the API key
Rotation immediately invalidates the old key. Store the returned key before making another API request.
curl -X POST https://filepost.dev/v1/rotate-key \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/rotate-key", {
method: "POST",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result.api_key);import os
import requests
response = requests.post(
"https://filepost.dev/v1/rotate-key",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json()["api_key"])<?php
$ch = curl_init("https://filepost.dev/v1/rotate-key");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $result["api_key"];Intake links
An intake link lets another person upload to your account without receiving
your API key. Share the returned upload_url, not the authenticated management
endpoint.
POST/v1/intake-linksCreate an intake link
curl -X POST https://filepost.dev/v1/intake-links \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label":"Accounts payable",
"allowed_types":["pdf"],
"max_file_size_mb":20,
"max_files":10,
"expires_in":"7d"
}'const response = await fetch("https://filepost.dev/v1/intake-links", {
method: "POST",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
label: "Accounts payable",
allowed_types: ["pdf"],
max_file_size_mb: 20,
max_files: 10,
expires_in: "7d"
})
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.post(
"https://filepost.dev/v1/intake-links",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
json={
"label": "Accounts payable",
"allowed_types": ["pdf"],
"max_file_size_mb": 20,
"max_files": 10,
"expires_in": "7d",
},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$payload = [
"label" => "Accounts payable",
"allowed_types" => ["pdf"],
"max_file_size_mb" => 20,
"max_files" => 10,
"expires_in" => "7d",
];
$ch = curl_init("https://filepost.dev/v1/intake-links");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));GET/v1/intake-linksList intake links
curl https://filepost.dev/v1/intake-links \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/intake-links", {
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.get(
"https://filepost.dev/v1/intake-links",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/intake-links");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));POST/v1/intake/{token}/uploadUpload through an intake link
This is a public multipart upload. It uses the token from the intake link and does not send your API key.
curl -X POST https://filepost.dev/v1/intake/3L18hphjnmDB/upload \
-F "file=@supplier-invoice.pdf;type=application/pdf"import { readFile } from "node:fs/promises";
const bytes = await readFile("supplier-invoice.pdf");
const form = new FormData();
form.append(
"file",
new Blob([bytes], { type: "application/pdf" }),
"supplier-invoice.pdf"
);
const response = await fetch(
"https://filepost.dev/v1/intake/3L18hphjnmDB/upload",
{ method: "POST", body: form }
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import requests
with open("supplier-invoice.pdf", "rb") as invoice:
response = requests.post(
"https://filepost.dev/v1/intake/3L18hphjnmDB/upload",
files={"file": (
"supplier-invoice.pdf",
invoice,
"application/pdf",
)},
timeout=60,
)
response.raise_for_status()
print(response.json())<?php
$file = new CURLFile(
"supplier-invoice.pdf",
"application/pdf",
"supplier-invoice.pdf"
);
$ch = curl_init(
"https://filepost.dev/v1/intake/3L18hphjnmDB/upload"
);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ["file" => $file],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));DELETE/v1/intake-links/{intake_id}Deactivate an intake link
Deactivation prevents new uploads. It does not delete files already received.
curl -X DELETE https://filepost.dev/v1/intake-links/3L18hphjnmDB \
-H "X-API-Key: $FILEPOST_API_KEY"const intakeId = "3L18hphjnmDB";
const response = await fetch(
`https://filepost.dev/v1/intake-links/${intakeId}`,
{
method: "DELETE",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
intake_id = "3L18hphjnmDB"
response = requests.delete(
f"https://filepost.dev/v1/intake-links/{intake_id}",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$intakeId = "3L18hphjnmDB";
$ch = curl_init(
"https://filepost.dev/v1/intake-links/" . $intakeId
);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));Webhooks
Set one webhook URL per account and FilePost sends a signed file.uploaded
event to it after every successful API upload (/v1/upload and
/v1/upload/base64). This lets your n8n, Zapier, Make, or custom backend react
to uploads without polling.
Delivery is retried with backoff until your endpoint responds successfully. Signatures let you confirm a request really came from FilePost.
PUT /v1/webhook: Set the webhook URL
Replaces the current webhook URL. Only https:// URLs are accepted in
production.
curl -X PUT https://filepost.dev/v1/webhook \
-H "X-API-Key: $FILEPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url":"https://your-app.example.com/hooks/filepost"}'const response = await fetch("https://filepost.dev/v1/webhook", {
method: "PUT",
headers: {
"X-API-Key": process.env.FILEPOST_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
webhook_url: "https://your-app.example.com/hooks/filepost"
})
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.put(
"https://filepost.dev/v1/webhook",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
json={"webhook_url": "https://your-app.example.com/hooks/filepost"},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/webhook");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("FILEPOST_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"webhook_url" => "https://your-app.example.com/hooks/filepost",
]),
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));GET/v1/webhookRead the webhook URL
Returns the configured URL, or null when none is set.
curl https://filepost.dev/v1/webhook \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/webhook", {
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.get(
"https://filepost.dev/v1/webhook",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/webhook");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("FILEPOST_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));DELETE/v1/webhookRemove the webhook
Stops all future events. Files already uploaded are unaffected.
curl -X DELETE https://filepost.dev/v1/webhook \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/webhook", {
method: "DELETE",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.delete(
"https://filepost.dev/v1/webhook",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/webhook");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("FILEPOST_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));POST/v1/webhook/testSend a test event
Sends an immediate ping event so you can verify your receiver before the next
upload. The response reports the receiver's HTTP status code.
curl -X POST https://filepost.dev/v1/webhook/test \
-H "X-API-Key: $FILEPOST_API_KEY"const response = await fetch("https://filepost.dev/v1/webhook/test", {
method: "POST",
headers: { "X-API-Key": process.env.FILEPOST_API_KEY }
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());import os
import requests
response = requests.post(
"https://filepost.dev/v1/webhook/test",
headers={"X-API-Key": os.environ["FILEPOST_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json())<?php
$ch = curl_init("https://filepost.dev/v1/webhook/test");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("FILEPOST_API_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
if ($body === false) throw new Exception(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) throw new Exception($body);
print_r(json_decode($body, true, 512, JSON_THROW_ON_ERROR));Event payload
Every event is a JSON POST with Content-Type: application/json.
{
"event": "file.uploaded",
"file_id": "8f3kX9aQ2v",
"url": "https://cdn.filepost.dev/8f3kX9aQ2v/report.pdf",
"size": 48291,
"content_type": "application/pdf",
"original_name": "report.pdf",
"filename_mode": "download",
"upload_source": "api",
"expires_at": null,
"uploaded_at": "2026-08-07T12:34:56+00:00"
}
The test event uses "event": "ping" with a sent_at timestamp instead.
Verifying signatures
Each request includes an X-FilePost-Signature header:
X-FilePost-Signature: sha256=<hex-digest>
The digest is an HMAC-SHA256 of the raw request body, keyed with the API key that performed the upload (or the key used to call the test endpoint). Any API key on the account verifies the same event if that key made the upload.
import hashlib
import hmac
def is_valid_filepost_signature(body: bytes, signature: str, api_key: str) -> bool:
expected = hmac.new(
api_key.encode(),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
Retry policy
Delivery runs after the upload response is sent, so a slow receiver never slows
down your uploads. FilePost retries up to 4 times total with backoff. Retries
stop as soon as the receiver answers 2xx or any 4xx (a permanent error such
as a wrong path); 5xx responses and network timeouts are retried.
Rate limits and plans
Rate limits are enforced per API key. Exceeding the rate limit returns
429 Too Many Requests.
| Account state | Uploads | Rate limit | Maximum file size | Storage |
|---|---|---|---|---|
| Free, unverified email | 15 per month | 10 requests/second | 50 MB | 2 GB |
| Free, verified email | 50 per month | 10 requests/second | 50 MB | 2 GB |
| Lite, $4/month | 300 per month | 30 requests/second | 100 MB | 10 GB |
| Starter, $9/month | 1,500 per month | 50 requests/second | 200 MB | Unlimited |
| Pro, $29/month | 7,500 per month | 200 requests/second | 500 MB | Unlimited |
All plans include HTTPS, Cloudflare CDN delivery, permanent URLs by default, and unlimited bandwidth.
Errors
All API errors use a JSON detail field:
{
"detail": "Human-readable error message"
}
| Status | Meaning | What to do |
|---|---|---|
400 |
Invalid request or file data | Check the documented body, filename, and content type. |
401 |
Missing or invalid API key | Confirm the X-API-Key header and active key. |
403 |
Plan or usage limit reached | Check account usage and plan limits. |
404 |
File or intake link not found | Confirm the ID belongs to the authenticated account. |
409 |
Conflicting account value | Use a different value or finish the pending change. |
410 |
Intake link expired, full, or inactive | Create or activate a usable intake link. |
413 |
File is too large | Use a smaller file or a plan with a larger limit. |
422 |
Validation failed | Read detail and correct the named field. |
429 |
Rate limit exceeded | Back off before retrying. |
500 |
Internal error | Retry with exponential backoff. |
502 |
Storage or CDN operation could not be confirmed | Keep the previous state and retry. Contact support if it persists. |
OpenAPI and interactive testing
The raw OpenAPI 3.1 schema is available at
/openapi.json. Use it with OpenAPI Generator, Orval, Kiota,
or another code generator to create a typed client.
Use the Swagger UI to inspect schemas and send individual requests interactively.
Documentation source
This reference is rendered from Markdown. The prose, tables, scenario, and
examples live in content/api-reference.md. Each :::code-tabs block must
contain cURL, JavaScript, Python, and PHP in that order. Updating the Markdown
updates the public documentation without editing page layout or JavaScript.
Ready to try the complete example?
Get a free API key and upload your first real file.