Skip to content
FormSubmit

Sending submissions

File uploads

Accept files with plain HTML forms or upload large files directly from the browser, stored privately with signed links.

FormSubmit stores uploaded files in private object storage. They're linked from your notification email, your dashboard and integration payloads using signed links that expire after 7 days. Nothing is publicly listed.

Limits

PlanPer fileTotal storage
Free10 MB100 MB
Starter10 MB250 MB
Pro10 MB1 GB
Business25 MB10 GB

Storage is shared across all your forms. Deleting a submission — manually or through retention — deletes its files and frees the space immediately.

Option 1: plain HTML (up to ~4.5 MB per request)

Add enctype="multipart/form-data" and a file input:

upload.html
<form action="https://formsubmit.app/f/YOUR_FORM_ID" method="POST" enctype="multipart/form-data">
  <input type="email" name="email" required>
  <input type="file" name="resume" accept=".pdf,.doc,.docx">
  <input type="file" name="photos" accept="image/*" multiple>
  <button type="submit">Send</button>
</form>

Option 2: direct browser uploads (up to your plan's per-file limit)

For large files, upload each file straight to storage from the browser, then submit the form with references to the uploaded files.

  1. Request a short-lived upload token from https://formsubmit.app/api/uploads using the Vercel Blob client, with clientPayload set to {"formId":"YOUR_FORM_ID"} and a pathname starting with f/YOUR_FORM_ID/.
  2. Upload the file.
  3. Submit the form with a _files field containing a JSON array of { field, url, filename }.
direct-upload.js
import { upload } from "@vercel/blob/client";

const FORM_ID = "YOUR_FORM_ID";

async function submit(form) {
  const data = new FormData(form);
  const files = [];

  for (const input of form.querySelectorAll('input[type="file"]')) {
    for (const file of input.files) {
      const blob = await upload(`f/${FORM_ID}/${file.name}`, file, {
        access: "private",
        handleUploadUrl: "https://formsubmit.app/api/uploads",
        clientPayload: JSON.stringify({ formId: FORM_ID }),
      });
      files.push({ field: input.name, url: blob.url, filename: file.name });
    }
    data.delete(input.name); // don't send the raw file again
  }

  data.set("_files", JSON.stringify(files));
  return fetch(`https://formsubmit.app/f/${FORM_ID}`, {
    method: "POST",
    body: data,
    headers: { Accept: "application/json" },
  }).then((r) => r.json());
}

The server verifies every referenced file belongs to your form, checks its real size against your plan, and attaches it to the submission. Files uploaded but never submitted are cleaned up automatically.

Where files show up

  • Email: a list of attachment links (turn this off in notification settings).
  • Dashboard: open a submission to preview images and PDFs or download any file.
  • Integrations: webhooks receive a files array with field, filename, url, size and contentType; Slack, Discord, Airtable and Google Sheets receive links.

Frequently asked questions

What file types are allowed?

Any type. Use the accept attribute on your input to guide visitors. Downloads of non-image, non-PDF files are always served as attachments for safety.

Do uploaded files count toward storage if the submission is spam?

Files attached to plain HTML submissions that are flagged as spam are not stored.