Resume upload form: build one that collects CVs you can use
Published
A resume upload form is a job application form with a file field: the applicant gives a few details, attaches a CV, and you receive both. You can have one running in minutes with a form builder such as Jotform, WPForms or Google Forms, or you can write it yourself in a few dozen lines of HTML and server code. The harder question is what happens after the upload, because a folder of attachments is not something you can search, filter or match against a job. This guide covers both routes, gives you the HTML for the do-it-yourself version, and shows how to turn each uploaded CV into structured data the moment it arrives.
Who needs which kind of resume upload form
There are two quite different readers behind this search, and the right answer depends on which one you are.
- A small team hiring now. You want applications in one place instead of scattered across email. A form builder is the right tool. You will read each CV yourself, so attachments are fine.
- A developer building a job board, careers page or ATS. The form is part of your product. You need control over the markup, validation, storage and what happens to the file next, and you will usually want the CV's contents as data rather than as a file someone has to open.
If you are in the first group, the next section is most of what you need. If you are in the second, skip to the HTML.
Option 1: a form builder
Every mainstream form builder offers a file upload field and a job application template. Here is how three common ones handle the upload itself, as of October 2026, read from each vendor's own pages.
| Jotform | WPForms | Google Forms | |
|---|---|---|---|
| Where it runs | Hosted, embed anywhere | Plugin inside your WordPress site | Hosted by Google |
| Free option | Starter plan | Free Lite plugin; the pricing page lists file uploads among paid-plan features | Free with a Google account |
| Where uploaded files go | Your Jotform account, with Google Drive, Dropbox and others as integrations | Your site, with Google Drive and Dropbox integrations | A new folder in the form owner's Google Drive |
| Applicant needs an account | No | No | Yes, a Google Account |
A few details from those pages are worth knowing before you choose.
Google Forms makes applicants sign in. Google's help page for the file upload question says: "To answer this question, responders need to sign in to a Google Account." For a public job posting that is real friction, and some applicants will leave at that point. It also says the question type is unavailable when the form is stored in a shared drive.
Jotform's free plan has limits and branding. Jotform's pricing FAQ describes a free Starter plan and paid Bronze, Silver and Gold plans, and says "Storage and submission limits increase with each level." Forms on the free plan carry Jotform branding until you pay.
WPForms lives inside WordPress. If your careers page is already a WordPress site, that is convenient: the form sits in your own pages and the uploads land on your own server, or in Google Drive or Dropbox through its integrations. If your site is not WordPress, it is not an option.
All three give you the same end result: a list of submissions, each with an attached file. That is fine when a person will open every CV. It stops being fine when you have a few hundred and want to know which applicants have five years of Python.
Option 2: build the resume upload form yourself
When the form is part of a product, write it yourself. The markup is short.
<form action="/apply" method="post" enctype="multipart/form-data">
<label>
Full name
<input name="full_name" autocomplete="name" required>
</label>
<label>
Email
<input name="email" type="email" autocomplete="email" required>
</label>
<label>
CV (PDF, Word .docx, or a photo or scan of the page, up to 20 MB)
<input name="cv" type="file" required
accept=".pdf,.docx,.png,.jpg,.jpeg,.webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,image/png,image/jpeg,image/webp">
</label>
<button type="submit">Apply</button>
</form>Three things in there do most of the work:
enctype="multipart/form-data"is what makes the browser send the file at all. Without it the server receives the filename and nothing else, which is a common reason a first upload form "works" and stores empty files.acceptfilters the file picker so applicants see the right files first. List both extensions and MIME types, because browsers and operating systems disagree about which one they honour.- The label states the formats and the size limit. An applicant who finds out about a limit only after a failed upload often does not try again.
accept is a convenience for the applicant. It does not stop anyone sending a different file, so everything it promises has to be checked again on the server.
The server side: checks that the browser cannot do
Whatever language your backend is in, the upload handler should do the same five things, in this order.
- Refuse oversize files early. Set a body size limit at the framework or proxy level so a 2 GB upload is cut off before it is read into memory.
- Check the file's real type from its first bytes. A PDF starts with
%PDF, a DOCX is a zip and starts withPK, a PNG and a JPEG each have their own signature. The filename and the browser'sContent-Typeare both just claims from the client. - Store it under a name you generate. Never use the applicant's filename as a path. Keep the original name as a field in your database if you want to show it later.
- Keep it private. CVs hold names, phone numbers, addresses and work history. Store them in a private bucket or folder, serve them only to signed-in staff, and decide how long you keep them. Our blind resume screening guide goes further on what to hide from whom.
- Answer with a clear outcome. Received, too large, wrong type, or could not be saved. Each should say what the applicant can do next, and the form should keep what they typed so a failed upload does not cost them the whole application.
A common gap is the legacy Word .doc format. Plenty of applicants still have one. Decide whether you accept it, and if you do not, say so in the label and in the error, and suggest they save it as PDF.
Turn each uploaded CV into data
This is where a hand-built form earns its keep. Once the file is validated and stored, you can send it to a parser and keep the result next to the application. Instead of a file called cv_final_v3.pdf, you get the applicant's roles, dates, education, skills and languages as fields.
Here is the extra step in a Node handler, after the checks above. It uses ResumeJSON, which reads PDF, DOCX, plain text and a photo or scan of the page, and accepts files up to 20 MB.
// after validating and storing the upload
const form = new FormData()
form.set('file', file, file.name)
const res = await fetch('https://resumejson-resume-cv-parser-api.p.rapidapi.com/v1/parse', {
method: 'POST',
headers: {
'x-rapidapi-key': process.env.RAPIDAPI_KEY,
'x-rapidapi-host': 'resumejson-resume-cv-parser-api.p.rapidapi.com'
},
body: form
})
if (res.ok) {
const { resume } = await res.json()
await db.applications.update(applicationId, { parsed: resume, parse_status: 'parsed' })
} else {
// keep the application; record that parsing failed so someone can look
await db.applications.update(applicationId, { parse_status: 'failed', parse_http: res.status })
}Two decisions in that snippet matter more than the code.
- The application is saved whether or not parsing succeeds. A parse is an enrichment. An applicant should never lose their application because a CV could not be read.
- The status is recorded either way.
parsedandfailedlook different in your database, so a CV that could not be read shows up as a row to check instead of as an applicant with no experience.
ResumeJSON's landing page states a median parse of 2.2 seconds, which is quick enough to run while the applicant is still on the page. That opens up a nicer form: parse on upload, then show the applicant the fields you read so they can correct them. Our job application autofill guide walks through that version step by step, including the field mapping.
With every CV stored as data, the rest gets easier: a searchable candidate database, candidate matching against a job's requirements, or a one-off export of every applicant to a spreadsheet with resume to Excel.
Form builder or your own form: a quick comparison
| Form builder | Your own form | |
|---|---|---|
| Time to a working form | Minutes | Some developer work, including the server checks |
| Control over markup and flow | Template and settings | Complete |
| Where files are stored | The vendor's storage or a connected drive | Wherever you choose |
| CV contents as searchable data | Not built in | Add a parser after upload |
| Prefill from the uploaded CV | Not built in | Possible once you parse on upload |
| Best for | A team reading each CV by hand | A job board, careers page or ATS |
When a simple upload form is enough
You do not need parsing, or even a custom form, in a lot of cases.
- You hire a few people a year. A form builder template and a shared inbox will do. Reading twenty CVs is not a problem worth engineering.
- Every CV is read by a person anyway. If nobody will search or filter, structured fields add nothing.
- You already use an ATS. Many applicant tracking systems ship their own application form and parse CVs themselves. Use theirs instead of building a second one beside it.
Parsing pays off when volume or product needs make reading by hand the bottleneck: a job board with many employers, a careers page that gets hundreds of applicants per role, or any tool that matches candidates to jobs.
Try it before you build
Before you write the upload handler, put a real CV through the free parser and look at the JSON it returns. That is exactly what your form's backend would store for each applicant. When you are ready to wire it in, the docs list every plan, including a free tier, and the full field reference.