Accept applications
With a key that lists jobs and accepts applications, your website can show its own application form and send applications to FindNeo. They arrive like applications from the FindNeo career page: same pipeline, same checks, and the candidate gets the same confirmation email.
It takes three calls:
getApplicationForm: which questions the job asks.uploadFile: the résumé, before applying.submitApplication: the answers.
Make these calls from the candidate's browser, not from your server. The limits on applications are per IP address (5 an hour), so a server sending every candidate's application from one address hits them almost at once.
1. Get the form
curl https://api.gofindneo.com/v1/website/jobs/JOB_ID/form \
-H "X-Api-Key: fnjobs_your_key"
Build your form from formFields, in order. Each field has:
| Field | What it is |
|---|---|
key |
The field's key. |
label, helpText |
What to show the candidate. |
dataType |
The kind of input: text, long_text, number, date, boolean, select, multi_select, file, or repeating (a group of children, such as each past job). |
isRequired |
Whether the form must have an answer. |
choices |
For select fields: { value, label }. Send the value. |
maxLength, minValue, maxValue |
Limits, where they apply. |
config |
Extra hints, such as format (email, phone, url), and for files accept (extensions) and maxBytes. |
answerKey |
Where the answer goes in the application, when it has a fixed place (see below). |
section |
A heading to group fields under, or null. |
screeningQuestions lists yes-or-no questions, each with an id and label.
Show every required field. Optional ones are up to you: a short form gets more applications.
2. Upload the résumé
Send the file's raw bytes, with its name in X-Filename:
async function uploadResume(jobId, file) {
const response = await fetch(`${API}/jobs/${jobId}/uploads`, {
method: 'POST',
headers: {
'X-Api-Key': KEY,
'Content-Type': 'application/octet-stream',
'X-Filename': encodeURIComponent(file.name),
},
body: file,
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
return (await response.json()).uploadId;
}
PDF, DOC and DOCX are accepted, up to 10 MB. FindNeo reads the type from the file itself, so a renamed file is refused with 415. The answer has uploadId and expiresAt: submit the application before then.
3. Submit the application
Build the body from the candidate's answers:
- A field with an
answerKeygoes at the top level of the body under that name, such asfirstName,emailorphone. The résumé's file field has ananswerKeytoo, usuallyresumeUploadId: put theuploadIdthere. - A field without an
answerKeygoes intocustomFields, under itskey. - Each screening question goes into
screeningAnswersas{ "questionId": "…", "answer": true }. consent: trueis required: the candidate's tick on "I agree to the processing of my data as described in the privacy notice". Without it the application is refused with422 ERR_CONSENT_REQUIRED. Show the checkbox and a link to the company's privacy notice, and sendtrueonly when it's ticked.
function buildBody(form, values, screening, uploadId) {
const body = { consent: true };
const customFields = {};
for (const field of form.formFields) {
const value = field.dataType === 'file' ? uploadId : values[field.key];
if (value === undefined || value === '' || value === null) continue;
if (field.answerKey) body[field.answerKey] = value;
else customFields[field.key] = value;
}
if (Object.keys(customFields).length > 0) body.customFields = customFields;
if (form.screeningQuestions.length > 0) {
body.screeningAnswers = form.screeningQuestions.map((q) => ({
questionId: q.id,
answer: screening[q.id] === true,
}));
}
return body;
}
async function submitApplication(jobId, body) {
const response = await fetch(`${API}/jobs/${jobId}/applications`, {
method: 'POST',
headers: { 'X-Api-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) throw Object.assign(new Error(result.detail ?? 'Not sent'), result);
return result.id;
}
Send numbers as numbers and yes-or-no answers as true or false. A 201 answer has the application's id: show the candidate a thank-you message.
A smallest possible body, for a form that asks only for the name, email, phone and résumé:
{
"firstName": "Asha",
"lastName": "Rao",
"email": "asha.rao@example.com",
"phone": "+91 98765 43210",
"resumeUploadId": "UPLOAD_ID",
"consent": true
}
When the answers are refused
A 422 ERR_VALIDATION_FAILED answer lists what's wrong in fields. Each entry's path points at the part of the body, such as /email or /customFields/notice_period, so you can show the message beside that input:
{
"code": "ERR_VALIDATION_FAILED",
"status": 422,
"title": "Validation failed",
"fields": [{ "path": "/email", "code": "ERR_FIELD_REQUIRED", "message": "Email is required." }]
}
A candidate (recognised by email) who already has an active application for the job is refused with 409 ERR_DUPLICATE, exactly as on the career page. The detail is written for the candidate, so you can show it as it is. See Errors and limits for every code.
Not used yet
botCheckToken is in the body's schema for a bot check that isn't switched on. Leave it out.