Features

File uploads

Accept resumes, screenshots, PDFs, or any other attachment alongside your form data. Files are stored privately and linked from the notification email.

The easy way: hosted forms

If you're using a hosted form, just add a File upload field in the builder. We handle the upload UI, progress, and storage end-to-end. Recipients get download links in their email; the files also live in your dashboard for as long as you keep the submission.

From your own HTML form

For forms hosted on your own site, you have two options:

Option 1: multipart/form-data

The simplest path — submit a normal HTML form with enctype="multipart/form-data". Files come through as attachments on the email.

html
<form
  action="https://formto.email/f/YOUR_FORM_ID"
  method="POST"
  enctype="multipart/form-data"
>
  <input name="name" required />
  <input name="email" type="email" required />
  <input name="resume" type="file" />
  <button type="submit">Send</button>
</form>

Option 2: direct-to-storage uploads (large files)

For files over a few MB, upload directly to our storage with a presigned URL, then submit the form with the upload IDs. This is the same flow our hosted forms use — fast, resumable, no proxy through the form endpoint.

  1. Request a presigned URL from POST /api/uploads/submission/presign with the form key, filename, mime type, and size.
  2. PUT the file to the returned uploadUrl.
  3. Submit the form with a hidden _uploadIds field set to the returned upload ID (comma-separated for multiple).
js
async function uploadAndSubmit(form) {
  const file = form.querySelector('input[type=file]').files[0];

  // 1. Presign
  const presignRes = await fetch("/api/uploads/submission/presign", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      formKey: "YOUR_FORM_ID",
      filename: file.name,
      mimeType: file.type,
      size: file.size,
      fieldName: "resume",
    }),
  });
  const presign = await presignRes.json();

  // 2. Upload directly to storage
  await fetch(presign.uploadUrl, {
    method: "PUT",
    headers: presign.headers,
    body: file,
  });

  // 3. Submit form with the upload ID
  const data = new FormData(form);
  data.delete("resume"); // file is uploaded; don't double-send the bytes
  data.set("_uploadIds", presign.uploadId);

  return fetch("https://formto.email/f/YOUR_FORM_ID", {
    method: "POST",
    headers: { Accept: "application/json" },
    body: data,
  });
}

Limits

  • Up to 10 files per submission.
  • Per-file size depends on your plan — see Limits.
  • Total storage is also capped per plan. Old files can be deleted from the submissions dashboard.
File uploads aren't available yet
The free tier doesn't include file storage. Paid plans with attachment support are coming soon.