Adding file uploads
Add file uploads to Qubeforms with multipart HTML forms or JavaScript. Learn attachment limits, multiple file fields, private downloads, and upload error handling.
1. Add the external form stylesheet
Save the CSS below as a separate file in your website. HTML examples load it from /static/assets/css/form-helper.css; React and Vue import ./form-helper.css beside the component. Adjust the path to match your site and confirm the file loads before publishing. The generic .form-helper class moves the text input off-screen without the hidden attribute or display: none. Keep tabindex="-1", autocomplete="off", and aria-hidden="true". New forms use fax_number; existing forms keep their saved name. Match Settings → Delivery & spam → Honeypot field in HTML, JSON, and drafts. To rename an existing trap, update both the setting and the website together. Never use a name that also collects a real visitor answer.
.form-helper {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
pointer-events: none;
}2. Before you begin
File uploads are included with Starter, Pro, and Business. Free forms cannot accept attachments. Create an active form, copy its public submission endpoint from your dashboard, and add your website to its allowed origins. Verify a recipient if you want notification emails. Customers submit files through Qubeforms and do not need their own storage account or AWS credentials. Only the public form key belongs in your browser form; read keys and other private credentials stay on your server.
3. Understand the limits
Each file must contain 1 byte to 5 MB (5,242,880 bytes). You can send up to 10 files per submission, counted across all file fields, for up to 50 MB of attachments. The 5 MB limit is per file, not an account storage allowance. Text fields and multipart encoding also count toward the overall request limit of 51 MB. Your form still follows its plan’s monthly submission allowance, origin rules, spam checks, and rate limits.
4. Add a file field to an HTML form
Replace cf_pk_YOUR_KEY below with your form’s public key. Use method="post" and enctype="multipart/form-data" so the browser sends file contents. Give every field a name. This example requires one PDF, JPEG, or PNG file and works without JavaScript. The empty fax_number field is the default spam trap; use your form’s configured honeypot name if you changed it. If CAPTCHA is enabled, include your configured challenge widget and its response token as well.
<link rel="stylesheet" href="/static/assets/css/form-helper.css">
<form
id="upload-form"
action="https://api.qubeforms.io/v1/cf_pk_YOUR_KEY/submissions"
method="post"
enctype="multipart/form-data"
>
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<label for="attachment">Attach a file</label>
<input
id="attachment"
name="attachment"
type="file"
accept=".pdf,.jpg,.jpeg,.png"
aria-describedby="upload-help"
required
>
<p id="upload-help">PDF, JPEG, or PNG. Maximum 5 MB per file.</p>
<input class="form-helper" name="fax_number" type="text" tabindex="-1" autocomplete="off" aria-hidden="true">
<button type="submit">Send</button>
<p id="upload-status" role="status" aria-live="polite"></p>
</form>5. Allow multiple files
To allow several selections, replace the single-file label, input, and help text above with this version. Add multiple and use an array field name such as attachments[] so every selected file reaches the submission. You can also use separate named file inputs. The combined count across all inputs must remain at or below 10 files. The accept attribute guides the file picker; it does not enforce file size or guarantee server approval.
<label for="attachments">Attach files</label>
<input
id="attachments"
name="attachments[]"
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png"
aria-describedby="upload-help"
required
>
<p id="upload-help">Up to 10 files. Maximum 5 MB each.</p>6. Submit with JavaScript
Optionally place this script after the form to keep visitors on the page. It works with either file-field example, checks size and count before uploading, and disables the button while sending. Use FormData and let the browser set Content-Type with the correct multipart boundary. Do not set that header yourself or convert the files to JSON. Accept: application/json requests a JSON response. The checks in this script improve feedback; Qubeforms also enforces its limits on the server.
<script>
const form = document.querySelector('#upload-form');
const status = document.querySelector('#upload-status');
const button = form.querySelector('button[type="submit"]');
const maxBytes = 5 * 1024 * 1024;
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button.disabled) return;
const body = new FormData(form);
const files = [...body.values()].filter((value) => value instanceof File);
if (files.length > 10) {
status.textContent = 'Choose no more than 10 files.';
return;
}
if (files.some((file) => file.size < 1 || file.size > maxBytes)) {
status.textContent = 'Each file must be between 1 byte and 5 MB.';
return;
}
button.disabled = true;
status.textContent = 'Uploading. Please keep this page open…';
try {
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body,
});
const result = await response.json().catch(() => null);
if (!response.ok) {
status.textContent = result?.error?.message
|| `Upload failed (HTTP ${response.status}).`;
return;
}
status.textContent = 'Your submission was received.';
form.reset();
} catch {
status.textContent = 'We could not confirm receipt. Check before submitting again.';
} finally {
button.disabled = false;
}
});
</script>7. Test a multipart request with curl
Replace the endpoint, local file paths, and Origin with your own values. The origin must be allowed by your form. --form sends real file contents and creates the multipart request; a filename in JSON does not upload a file. No read key is needed to submit. This example assumes your form does not require a CAPTCHA token.
curl --fail-with-body \
'https://api.qubeforms.io/v1/cf_pk_YOUR_KEY/submissions' \
--header 'Accept: application/json' \
--header 'Origin: https://www.example.com' \
--form 'email=alex@example.com' \
--form 'message=Please review these files.' \
--form 'fax_number=' \
--form 'attachments[]=@/path/to/document.pdf' \
--form 'attachments[]=@/path/to/photo.jpg'8. Choose appropriate file types
PDFs and common raster images such as JPEG and PNG are useful choices for ordinary attachment forms. Qubeforms checks the detected file type and rejects HTML, SVG, PHP, certain executable and shell-script types, and types reported as text/x-*. Renaming a file does not make its contents acceptable. If a file is rejected, export it to a suitable document or image format and try again. File-type checks are not a guarantee that a file is safe; treat files from unknown submitters with care.
9. Handle success and redirects
A successful JSON submission returns HTTP 201. Receipt does not guarantee that a notification email has already arrived or that spam screening placed the record in the Inbox; inspect the submission and its delivery status. A native HTML form normally shows the API response unless you have configured a permitted thank-you redirect. The JavaScript example requests JSON and shows its own success message. If a connection fails after sending, check for receipt before repeating the submission, since an uncertain response can still mean the server accepted it.
10. Download submitted files
Notification emails include Secure attachments links rather than attaching the file bytes to the email. Those links are generated with a seven-day lifetime. Your server can also call GET /v1/read/submissions/{submissionId} with the form’s secret read key and read each uploads entry’s download_url. Links generated by this API have a 15-minute lifetime starting when they are generated, not when the visitor submits the form. Request the submission again to obtain fresh links. Keep download links private: someone holding a valid link can use it to access the file. For this server-side example, store your form’s read key in QUBEFORMS_READ_KEY and replace SUBMISSION_ID with an ID returned by the read API. Create the key under Settings, then API & routing.
curl --fail-with-body \
'https://api.qubeforms.io/v1/read/submissions/SUBMISSION_ID' \
--header "Authorization: Bearer ${QUBEFORMS_READ_KEY}" \
--header 'Accept: application/json'11. Understand file retention
Files share their submission’s retention deadline. The default is 90 days on Starter and 365 days on Pro and Business, measured from receipt; a form may have a shorter retention period. Expired files are removed by scheduled cleanup. A link’s expiration does not delete the file, and generating a fresh link does not extend retention. Files removed by cleanup cannot be downloaded even if a link has time remaining. Save a copy before the retention deadline if you need to keep a file longer.
12. Use uploads with multi-step forms
Draft creation and update endpoints save form fields, not file attachments. Keep selected files in the browser until the final step, then send them to the submission endpoint using FormData or a multipart HTML form. When completing a saved draft, include _cf_draft_id and _cf_draft_token with that final request. Reloading or closing the page can require the visitor to select the files again; a saved draft does not preserve them.
13. Troubleshoot upload errors
Read error.message in a JSON error response. HTTP 402 means the form’s plan does not allow uploads. HTTP 413 means the request is too large. HTTP 422 can mean too many files, an empty or oversized file, a blocked type, an upload transfer problem, or another form validation failure. HTTP 429 means a rate or usage limit was reached; follow Retry-After when present and check the account’s usage. HTTP 403 can indicate a disallowed origin; HTTP 404 can indicate an unavailable form. HTTP 500 or HTTP 503 can indicate a service or storage problem. If these persist, contact support with the time and form endpoint, without sharing private keys or download links.
14. Check your integration before publishing
Submit a small test file from your real website origin, verify the submission arrived, and open its notification or read-API download link. Test a file over 5 MB and more than 10 files to confirm visitors see useful errors. For missing files, check enctype, field names, attachments[] for multiple selections, and that JavaScript sends FormData. For browser CORS errors, check the form’s allowed origins and do not use mode: no-cors, which prevents your script from reading the response. See the AJAX and JSON submissions, Allowed origins and CORS, and API authentication guides for related setup.
Public form keys can appear in browser code. Read keys, management credentials, and webhook signing secrets must stay on the server.