File uploads

Form Plume supports file fields for resumes, screenshots, signed PDFs, photos, and other submission attachments. Files are stored separately from the submission's text fields, then linked back to the submission in the dashboard.

There are two ways to upload files:

MethodBest for
Plain multipart formNo-JS sites, simple contact forms, small attachments
Presigned uploadsJavaScript apps, progress bars, larger client-side flows

The default policy is 10 files per submission, 10 MB per file, and storage is counted against the tenant's plan.

Plain HTML uploads

For a normal HTML form, add enctype="multipart/form-data" and use a file input. The browser sends the file bytes directly to Form Plume with the rest of the form.

<form
  action="https://api.formplume.com/f/{public_slug}"
  method="POST"
  enctype="multipart/form-data"
>
  <label>
    Name
    <input name="name" required>
  </label>
 
  <label>
    Email
    <input type="email" name="email" required>
  </label>
 
  <label>
    Resume
    <input type="file" name="resume" accept=".pdf,.doc,.docx">
  </label>
 
  <button type="submit">Send</button>
</form>

That is the whole setup. Text fields land in the submission body. File fields become attachments linked to the submission.

Use multiple when a field can include more than one file:

<input type="file" name="screenshots" accept="image/*" multiple>

Plain multipart uploads are the easiest path for most forms. If you need upload progress, retries, or want file bytes to bypass the Form Plume API process, use the presigned flow below.

Presigned uploads

The presigned flow is a three-step process:

  1. Ask Form Plume for temporary upload slots.
  2. Upload each file directly to object storage with the returned URL and headers.
  3. Submit the form text and include _uploads references so Form Plume can attach those files to the submission.

Use this flow when you are building a JavaScript app or custom uploader.

1. Request upload slots

Call the presign endpoint with the files you intend to upload:

const file = document.querySelector('input[name="resume"]').files[0];
 
const presignRes = await fetch(
  "https://api.formplume.com/v1/forms/{public_slug}/uploads/presign",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({
      files: [
        {
          field: "resume",
          filename: file.name,
          content_type: file.type || "application/octet-stream",
          size: file.size,
        },
      ],
    }),
  },
);
 
const { uploads } = await presignRes.json();

For public forms, no auth header is required. For protected forms, send the same access key you use to submit:

headers: {
  "Content-Type": "application/json",
  "Accept": "application/json",
  "Authorization": "Bearer fp_live_...",
}

The path segment accepts either the form UUID or the public slug.

2. Upload the bytes

Each upload slot includes a short-lived URL, method, headers, and object key. Use the URL exactly as returned:

const slot = uploads[0];
 
await fetch(slot.url, {
  method: slot.method,
  headers: slot.headers,
  body: file,
});

The slot's key is not a download URL. It is the storage reference you pass back to Form Plume in the final submission.

3. Submit the form

Send your normal submission and include _uploads.

For JSON submissions, _uploads can be an array:

const submitRes = await fetch("https://api.formplume.com/f/{public_slug}", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({
    name: "Ada Lovelace",
    email: "ada@example.com",
    message: "Here is my resume.",
    _uploads: uploads.map((slot) => ({
      field: slot.field,
      key: slot.key,
    })),
  }),
});
 
const result = await submitRes.json();

For a normal form post, _uploads must be a JSON string:

<input
  type="hidden"
  name="_uploads"
  value='[{"field":"resume","key":"t/tenant/form/2026/07/.../resume.pdf"}]'
>

Form Plume links any matching pending upload to the new submission. Unknown, expired, foreign, or already-linked keys are ignored so a bad upload reference does not block the text submission.

Downloading files

Files are downloaded from the dashboard or the Form Plume API. The download endpoint returns a short-lived redirect to object storage:

GET /v1/submissions/{submission_id}/files/{file_id}/download

Only authenticated dashboard/API callers from the owning tenant can download submission files.

Errors

Upload validation errors are machine-readable:

CodeWhat it means
uploads_unavailableFile storage is not configured
no_filesThe presign request did not include any files
too_many_filesThe submission has more files than the form allows
invalid_fileA file was empty, unreadable, or had an invalid size
file_too_largeA file exceeds the form's per-file byte limit
unsupported_typeA file's content type is not allowed
storage_quota_exceededThe tenant has reached the plan's storage limit
file_not_readyA presigned upload has not finished finalizing yet

See Success and errors for the full response shape.

On this page