Make one real URL before you keep reading

Upload a file up to 10 MB. The public URL works immediately, with no password or credit card. Executable files require email verification.

Secure Browser File Uploads Without Exposing Your API Key

· 8 min read

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

  1. The browser asks your server for an upload link.
  2. Your server calls POST /v1/intake-links with the API key.
  3. Your server returns the public upload_url and its expiry to the browser.
  4. 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:

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

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.

Get Your Free API Key

Related FilePost guides

Further reading