FindNeo Developers

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:

  1. getApplicationForm: which questions the job asks.
  2. uploadFile: the résumé, before applying.
  3. 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 answerKey goes at the top level of the body under that name, such as firstName, email or phone. The résumé's file field has an answerKey too, usually resumeUploadId: put the uploadId there.
  • A field without an answerKey goes into customFields, under its key.
  • Each screening question goes into screeningAnswers as { "questionId": "…", "answer": true }.
  • consent: true is 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 with 422 ERR_CONSENT_REQUIRED. Show the checkbox and a link to the company's privacy notice, and send true only 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.