8 fields
Job application form
A job application form that collects the role, a CV link, portfolio and cover letter, with consent, validated on the server before it reaches your hiring sheet.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
Copy the code
Every version below is generated from the same field definition. Replace frm_xxxxxxxxxxxxxxxx with your form id, or enter it in the Form ID box and the code updates.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="full_name">Full name</label>
<input id="full_name" name="full_name" type="text" maxlength="160" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" />
<label for="role">Role</label>
<select id="role" name="role" required>
<option value="">Choose one</option>
<option value="frontend_engineer">Frontend engineer</option>
<option value="backend_engineer">Backend engineer</option>
<option value="product_designer">Product designer</option>
</select>
<label for="cv_url">Link to your CV</label>
<input id="cv_url" name="cv_url" type="url" required aria-describedby="cv_url-help" />
<p id="cv_url-help">A public or shared link, for example Google Drive, Dropbox or your own site.</p>
<label for="portfolio_url">Portfolio or GitHub</label>
<input id="portfolio_url" name="portfolio_url" type="url" />
<label for="cover_letter">Why this role?</label>
<textarea id="cover_letter" name="cover_letter" rows="8" maxlength="6000"></textarea>
<label><input name="consent" type="checkbox" required /> Keep my application on file for this hiring round</label>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Apply</button>
</form>- HTML needs no JavaScript. After a successful post the browser is redirected to the form's redirect URL, or to Ikiform's hosted thank-you page.
- React uses the SDK:
npm install @ikiform/sdk. Field errors from the server appear under each field. - JavaScript enhances the HTML form with
fetch, so the visitor stays on the page. - The hidden
_gotchainput is the honeypot. Leave it empty and hidden.
challenge(FORM_ID) from @ikiform/sdk and pass it to submit as turnstileToken, or add it to the fetch body as cf-turnstile-response. Without it the post is refused with 403 challenge_required. See the Next.js guide and Spam protection.The fields
Paste this definition on the form's Fields tab in the dashboard. Ikiform then checks every post against it on the server, refuses bad data with 422 validation_failed, refuses any field that isn't listed, and stores typed values.
| Field id | Type | Required | Accepts |
|---|---|---|---|
full_name | text | Yes | Up to 160 characters |
email | email | Yes | One valid address |
phone | phone | No | 6 to 15 digits |
role | select | Yes | One of: frontend_engineer, backend_engineer, product_designer |
cv_url | url | Yes | An https: URL |
portfolio_url | url | No | An https: URL |
cover_letter | textarea | No | Up to 6,000 characters |
consent | checkbox | Yes | Must be checked, stored as true |
{
"version": 1,
"title": "Job application",
"fields": [
{
"id": "full_name",
"type": "text",
"label": "Full name",
"required": true,
"validation": {
"max_length": 160
}
},
{
"id": "email",
"type": "email",
"label": "Email",
"required": true
},
{
"id": "phone",
"type": "phone",
"label": "Phone"
},
{
"id": "role",
"type": "select",
"label": "Role",
"required": true,
"options": [
{
"value": "frontend_engineer",
"label": "Frontend engineer"
},
{
"value": "backend_engineer",
"label": "Backend engineer"
},
{
"value": "product_designer",
"label": "Product designer"
}
]
},
{
"id": "cv_url",
"type": "url",
"label": "Link to your CV",
"required": true,
"help_text": "A public or shared link, for example Google Drive, Dropbox or your own site.",
"validation": {
"https_only": true
}
},
{
"id": "portfolio_url",
"type": "url",
"label": "Portfolio or GitHub",
"validation": {
"https_only": true
}
},
{
"id": "cover_letter",
"type": "textarea",
"label": "Why this role?",
"rows": 8,
"validation": {
"max_length": 6000
}
},
{
"id": "consent",
"type": "checkbox",
"label": "Keep my application on file for this hiring round",
"required": true
}
],
"settings": {
"submit_label": "Apply",
"success_message": "Thanks for applying. We reply to every application."
}
}Next.js server action
To keep the form working without JavaScript in a Next.js app, post from a server action with a secret key. Create the key under API keys with the submissions:write scope and keep it out of the browser.
IKIFORM_SECRET_KEY=ik_sk_your_secret_key"use server";
export interface JobApplicationFormState {
ok: boolean;
message: string;
fields?: Record<string, string[]>;
values?: Record<string, string[]>;
}
export async function sendJobApplicationForm(_previous: JobApplicationFormState, formData: FormData): Promise<JobApplicationFormState> {
const body = new URLSearchParams();
for (const [name, value] of formData) {
if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
}
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
},
body,
});
const result = await response.json();
if (result.ok) return { ok: true, message: "Thanks for applying. We reply to every application." };
const values = Object.fromEntries([...new Set(body.keys())].map((name) => [name, body.getAll(name)]));
return { ok: false, message: result.error.message, fields: result.error.fields, values };
}"use client";
import { useActionState } from "react";
import { type JobApplicationFormState, sendJobApplicationForm } from "./actions";
const initial: JobApplicationFormState = { ok: false, message: "" };
export function JobApplicationForm() {
const [state, action, pending] = useActionState(sendJobApplicationForm, initial);
if (state.ok) return <p>{state.message}</p>;
return (
<form action={action}>
<label htmlFor="full_name">Full name</label>
<input id="full_name" name="full_name" type="text" maxLength={160} required defaultValue={state.values?.full_name?.[0]} />
{state.fields?.full_name ? <p>{state.fields.full_name[0]}</p> : null}
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" autoComplete="email" required defaultValue={state.values?.email?.[0]} />
{state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
<label htmlFor="phone">Phone</label>
<input id="phone" name="phone" type="tel" autoComplete="tel" defaultValue={state.values?.phone?.[0]} />
{state.fields?.phone ? <p>{state.fields.phone[0]}</p> : null}
<label htmlFor="role">Role</label>
<select id="role" name="role" required defaultValue={state.values?.role?.[0]}>
<option value="">Choose one</option>
<option value="frontend_engineer">Frontend engineer</option>
<option value="backend_engineer">Backend engineer</option>
<option value="product_designer">Product designer</option>
</select>
{state.fields?.role ? <p>{state.fields.role[0]}</p> : null}
<label htmlFor="cv_url">Link to your CV</label>
<input id="cv_url" name="cv_url" type="url" required aria-describedby="cv_url-help" defaultValue={state.values?.cv_url?.[0]} />
<p id="cv_url-help">A public or shared link, for example Google Drive, Dropbox or your own site.</p>
{state.fields?.cv_url ? <p>{state.fields.cv_url[0]}</p> : null}
<label htmlFor="portfolio_url">Portfolio or GitHub</label>
<input id="portfolio_url" name="portfolio_url" type="url" defaultValue={state.values?.portfolio_url?.[0]} />
{state.fields?.portfolio_url ? <p>{state.fields.portfolio_url[0]}</p> : null}
<label htmlFor="cover_letter">Why this role?</label>
<textarea id="cover_letter" name="cover_letter" rows={8} maxLength={6000} defaultValue={state.values?.cover_letter?.[0]} />
{state.fields?.cover_letter ? <p>{state.fields.cover_letter[0]}</p> : null}
<label><input name="consent" type="checkbox" required defaultChecked={Boolean(state.values?.consent)} /> Keep my application on file for this hiring round</label>
{state.fields?.consent ? <p>{state.fields.consent[0]}</p> : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Apply"}</button>
</form>
);
}Set it up
- Create a form in the dashboard and copy its id.
- Paste the field definition on the Fields tab.
- Add your site under Allowed domains, plus
localhostwhile you develop. - Set the rate limit to Strict, 2 every 10 minutes per visitor if visitors post from the browser. One person applies once. Strict still leaves room to fix a typo and send again. With the server action, every post comes from your server's IP, so choose Relaxed or Custom instead.
- Paste the code, send a test and check it arrives in the dashboard.
CVs as links, not files
Ikiform doesn't accept file uploads yet. A post that contains a real file is refused with 422 files_not_supported; an empty file input is ignored. Ask for a link instead, as this form does, or upload the CV to your own storage first and submit its URL. The file upload form shows how.
https_only on the URL fields refuses http: links, and the stored value is the normalized URL, with https:// added when the applicant leaves it out.
Size limits that matter
| Limit | Value |
|---|---|
| One field value | 16 KB |
| All stored fields together | 64 KB |
| The whole request | 256 KB |
Long text field, maximum max_length | 16,000 characters |
The value limit is in bytes, not characters, so a 16,000 character cover letter in a script that uses several bytes per character can pass max_length and still hit the 16 KB limit with 413 payload_too_large. The 6,000 character cap here stays clear of it.
Links and the spam score
This form can already send two links, cv_url and portfolio_url. Three or more links across all fields add 2 to the spam score, so when both are filled in, one more link in the cover letter is enough. With an identical repeat (+3) that reaches the threshold of 5, and the application is stored as spam without an error. Spam sends no webhooks or emails, so review spam submissions in the dashboard. Marking one as not spam sends its webhooks then.
Personal data
- The required
consentcheckbox is stored astrue, next to the submission's timestamp, so you have a record of what each applicant agreed to. - Don't ask for passwords, card numbers or national ID numbers. Field names that look like credentials, such as
ssn, are flagged and their notification emails held for review. - Delete an application from the dashboard or with
DELETE /submissions/:idin the REST API when the hiring round ends.
Where to send it
Every submission can go to a signed webhook. These guides cover the tools that suit this form, with the relay code:
- Send form submissions to Airtable
- Send form submissions to a Notion database
- Send form submissions to Google Sheets
- Send form submissions to Slack
Questions
Not directly. Ikiform refuses posts with files (422 files_not_supported). Ask for a link, or upload the file to your own storage such as S3 or R2 and submit the resulting URL in a url field.
A long text field allows up to 16,000 characters, but every field value is also limited to 16 KB. This form caps the cover letter at 6,000 characters to stay well inside both.
Somewhere the hiring team can review them side by side: an Airtable base, a Notion database or a Google Sheet, fed by a signed webhook, with a Slack message per application if you want a heads-up.