A browser is a useful place to choose a file, but it is a bad place to keep a long-lived API key. Anything shipped to the browser can be inspected, copied, and replayed. That does not make browser uploads impossible; it means the browser needs a narrower capability than your account credential.
This tutorial uses a FilePost intake link as that capability. Your server creates a link with the API key, applies the file rules, and gives the browser only the resulting upload link. The browser can then send a file without learning the key that manages your account.
The pattern
- The browser asks your server for an upload link.
- Your server calls
POST /v1/intake-linkswith the API key. - Your server returns the public
upload_urland its expiry to the browser. - The user uploads through the hosted FilePost page, which returns a public file URL.
The important boundary is step two: the management request happens server-side. The public link is deliberately limited by expiry, file type, file count, and size. It is not a replacement for authentication on your own application; it is a scoped upload capability for a specific workflow.
1. Keep the API key on your server
Start with an environment variable. Do not put this value in HTML, a React bundle, a mobile app, or a public repository.
FILEPOST_API_KEY=fh_your_server_side_key
PORT=3000
Here is a minimal Node.js 18+ route. Node 18 includes fetch, so this example needs only Express for the HTTP server.
import express from "express";
const app = express();
app.use(express.json());
const apiKey = process.env.FILEPOST_API_KEY;
if (!apiKey) throw new Error("FILEPOST_API_KEY is required");
app.post("/api/upload-link", async (req, res) => {
const response = await fetch("https://filepost.dev/v1/intake-links", {
method: "POST",
headers: {
"X-API-Key": apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
label: "Contact form attachments",
allowed_types: ["pdf", "png", "jpg", "jpeg"],
max_file_size_mb: 20,
max_files: 1,
expires_in: "24h"
})
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({
detail: data.detail || "Could not create upload link"
});
}
res.json({
upload_url: data.upload_url,
expires_at: data.expires_at
});
});
app.listen(process.env.PORT || 3000);
The client is allowed to call your /api/upload-link route because it does not contain a secret. The route makes the authenticated FilePost request and deliberately returns only the fields the client needs.
2. Let the browser open the constrained upload page
The simplest client UI does not handle file bytes at all. It asks for a link and sends the user to FilePost's hosted upload page. The page provides file selection, size/type checks, upload progress, and the resulting URL.
<button id="create-link">Choose a file to upload</button>
<p id="status" role="status"></p>
<script>
const button = document.querySelector("#create-link");
const status = document.querySelector("#status");
button.addEventListener("click", async () => {
button.disabled = true;
status.textContent = "Preparing a secure upload link...";
try {
const response = await fetch("/api/upload-link", { method: "POST" });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Request failed");
window.location.assign(data.upload_url);
} catch (error) {
status.textContent = error.message;
button.disabled = false;
}
});
</script>
This flow is useful for support forms, supplier document collection, job applications, and any other case where a person needs to send a file to your team. You can also email the returned upload_url or render it as a link instead of navigating immediately.
3. Apply the smallest useful constraints
Do not make every upload link an unlimited bucket. Set the rules close to the workflow:
-
allowed_typesaccepts extensions such aspdforpng. -
max_file_size_mbprevents a document workflow from accepting unexpectedly large files. -
max_filescan make a link single-use or cap a small batch. -
expires_inaccepts values such as24hor7d. -
webhook_urlcan notify your server after a successful intake upload.
After the upload, your server can store the returned URL, send a notification, or process the file. If you configure a webhook, verify the X-FilePost-Signature before trusting the event; the upload webhook guide shows the verification pattern.
4. When you need a custom upload UI
The hosted page is the portable default. If you need to keep the user inside your own interface, the intake response also includes an intake_id. Your browser can send multipart data to the public intake endpoint without an API key:
const formData = new FormData();
formData.append("file", fileInput.files[0]);
const response = await fetch(
`https://upload.filepost.dev/v1/intake/${encodeURIComponent(intakeId)}/upload`,
{ method: "POST", body: formData }
);
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Upload failed");
console.log(data.url);
Use this version only when your frontend origin is allowed by the upload host's CORS policy. For a different origin, keep the hosted page or add a server-side proxy; do not “solve” the problem by exposing your account key. In either version, treat the intake URL as a bearer capability: share it only with the intended uploader and give it the shortest useful lifetime.
Security checklist
- Keep
FILEPOST_API_KEYin server-side environment configuration. - Return
upload_url, not the authenticated intake-link management endpoint. - Use an expiry, file type allowlist, size limit, and file-count limit.
- Validate the terminal response or webhook before writing the URL to your database.
- Deactivate a link early when the workflow is complete:
DELETE /v1/intake-links/{intake_id}. - Keep your own application authorization separate from the upload capability.
If the upload is initiated by a trusted server or automation workflow, use the regular authenticated API upload instead. Intake links are for the specific case where an untrusted browser or outside person needs to contribute a file without receiving your account credential.
Build a file drop without shipping a secret
Create a constrained intake link, send the hosted upload page to a user, and keep your API key on the server. Start with the FilePost API docs.
Top comments (0)