> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jobmojito.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pre-screen candidates

> Score a résumé against a position before inviting the candidate to a full interview.

Pre-screening scores a candidate's résumé (and optional form answers) against a position, returning an AI recommendation. It's two steps: define the pre-screening position, then submit candidates.

| Step | Endpoint | MCP tool |
| - | - | - |
| Create/update a pre-screening position | `POST /pre-screening-create` | `upsert_pre_screening` |
| Screen a candidate (plain text résumé) | `POST /job-interview-pre-screening-api-resume-text` | `pre_screen_resume_text` |
| Screen a candidate (uploaded file) | `POST /job-interview-pre-screening-api-resume-binary` | `pre_screen_resume_binary` |

## 1. Create the pre-screening position

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://cool.jobmojito.com/functions/v1/pre-screening-create \
    -H "Authorization: Bearer $SUPABASE_JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "position_name": "Senior Backend Engineer",
      "position_location": "Remote (EU)",
      "position_country_code": "DE",
      "mojito_language_code": "en",
      "type": "resume",
      "status": "active",
      "position_description": "Backend role: Python, async APIs, AWS.",
      "candidate_expectations": "5+ yrs Python, cloud, SQL."
    }'
  ```
</CodeGroup>

When creating, `position_name`, `position_location`, `position_country_code`, and `mojito_language_code` are required. `type` is `resume`, `form`, or `resume_with_form`. To **update** an existing position, pass `update_position_def_set_id` instead and only the fields you want to change.

The response returns `position_def_set_id` (use it to screen candidates) and `interview_pre_screening_id`.

### Configuring the screening rules

What the screen actually checks is defined by two arrays on the request: **`assessment_rules`** (the checks that pass / flag / reject a candidate) and **`form_fields`** (the candidate-facing form). Both are stored as-is and **replaced wholesale on update** — always send the complete array, never a partial delta.

<Warning>
  A rule only screens candidates when its `type` is `"screening"`. A rule with `type: "required"` or `"optional"` (or a resume rule you enabled without any threshold) is collected in the form but never rejects or flags anyone. The two resume-scoring rules — `resume_match` and `resume_technical_experience` — are always enforced when present and take no `type`.
</Warning>

#### `assessment_rules` — the checks

An **array** of rule objects. Each rule has an `id` (which check), an optional `type` (`optional` | `required` | `screening`), an optional `action` for when a **screening** rule is not met (`mark_for_review` | `reject`, default `mark_for_review`), and id-specific keys:

| `id` | Applies to `type` | Extra keys | Passes when |
| - | - | - | - |
| `form_nationality` | form | `country` (string\[] of ISO codes), `country_eu` (bool) | Candidate nationality is in `country` **or** (`country_eu` and it's an EU country) |
| `form_residency` | form | `country`, `country_eu` | Candidate residency is in `country` **or** an EU country |
| `form_languages` | form | `languages`: `[{ code, level }]` | Candidate knows every listed language at **at least** the given `level` |
| `form_education` | form | `education` (level key) | Candidate education ≥ `education`. Use `form_education_by_country` to screen against a country-specific ladder |
| `form_visa` | form | `visa` (string\[] of visa keys) | Candidate visa is in the list. Visa keys are country-specific (only some countries, e.g. `KW`, `TR`) |
| `form_age` | form | `age_min`, `age_max` | Candidate age is within `[age_min, age_max]` |
| `form_gender` | form | `gender` (string\[] of gender keys) | Candidate gender is in the list |
| `form_cover_letter` | form | — | File upload only — collected, never screened |
| `form_national_id` | form | — | File upload only — collected, never screened |
| `resume_match` | resume | `resume_score_reject`, `resume_score_accept` (0–10) | AI match score ≥ `resume_score_accept` passes; between the two → mark for review; below `resume_score_reject` (when > 0) → reject |
| `resume_technical_experience` | resume | `technical_experience_years` | AI-detected years of relevant experience ≥ the value |
| `resume_education` | resume | `education` (level key) | AI-detected education ≥ `education` (or `resume_education_by_country`) |

**Allowed keys:**

* `education`: `primary school`, `high school no graduation`, `high school`, `vocational`, `bachelors`, `masters`, `doctorate`
* language `level`: `beginner`, `intermediate`, `advanced`, `fluent`
* `gender`: `male`, `female`, `other`

<Note>
  `form_age` and `form_gender` are unavailable (and ignored) for positions in EU / GDPR countries. `form_visa` is only available where visa keys are defined for the position country.
</Note>

#### `form_fields` — the candidate form

An **array** describing what the candidate fills in. Two kinds of entry:

* **Standard field reference** — `{ "id": "form_education", "mandatory": true }`. Mirrors a standard `assessment_rules` entry so the field is shown in the form (`mandatory` = the rule's `type` is not `optional`). Enable a standard form field in **both** arrays.
* **Custom field** — `{ "id": "custom_<timestamp>", "title": "...", "type": "required", "field_type": "text" }`. A free-form question you add. `field_type` is `text` (free text), `file` (upload) or `radio` (single choice — supply `options: ["...", "..."]`, at least two). Custom fields are collected but **not** evaluated by the screening engine.

`resume`-only positions ignore `form_fields`; `form` and `resume_with_form` positions require at least one entry.

#### Full form + resume example

<CodeGroup>
  ```bash Form + Resume position theme={null}
  curl -X POST https://cool.jobmojito.com/functions/v1/pre-screening-create \
    -H "Authorization: Bearer $SUPABASE_JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "position_name": "Customer Support Specialist",
      "position_location": "Remote",
      "position_country_code": "US",
      "mojito_language_code": "en",
      "type": "resume_with_form",
      "status": "active",
      "assessment_rules": [
        { "id": "form_education",  "type": "screening", "action": "reject",          "education": "bachelors" },
        { "id": "form_languages",  "type": "screening", "action": "mark_for_review", "languages": [{ "code": "en", "level": "advanced" }] },
        { "id": "form_nationality","type": "required" },
        { "id": "resume_match", "resume_score_reject": 2, "resume_score_accept": 7 },
        { "id": "resume_technical_experience", "action": "mark_for_review", "technical_experience_years": 3 }
      ],
      "form_fields": [
        { "id": "form_education",   "mandatory": true },
        { "id": "form_languages",   "mandatory": true },
        { "id": "form_nationality", "mandatory": true },
        { "id": "custom_1772004521633", "title": "Do you hold a valid work permit?", "type": "required", "field_type": "radio", "options": ["Yes", "No"] }
      ]
    }'
  ```
</CodeGroup>

In this example a candidate below a bachelor's degree is **rejected**; one who lacks advanced English, has fewer than 3 years of experience, or scores 2–6 on the AI résumé match is **marked for review**; nationality and the work-permit question are collected but not screened.

## 2. Screen a candidate

Submit the candidate against the `position_id` (the `position_def_set_id` from step 1). Required: `position_id`, `candidate_name`, `candidate_email`, `candidate_resume`.

`candidate_country_code` is **optional**. It's used only as the fallback for `residency`/`nationality` when those aren't given in `form`.

<Warning>
  If the position has a `form_nationality` or `form_residency` **screening** rule and you send neither `candidate_country_code` nor the matching `form.nationality` / `form.residency`, the candidate is screened with no nationality/residency and **fails that rule** — marked for review or rejected per the rule's `action`. Supply the country code (or the `form` value) whenever such a rule is configured.
</Warning>

<CodeGroup>
  ```bash Plain-text résumé theme={null}
  curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-pre-screening-api-resume-text \
    -H "Authorization: Bearer $SUPABASE_JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "position_id": "POSITION_DEF_SET_UUID",
      "candidate_name": "Peter Parker",
      "candidate_email": "peter@parker.com",
      "candidate_country_code": "US",
      "candidate_resume": "Full plaintext résumé here…",
      "candidate_linkedin_url": "https://linkedin.com/in/peterparker",
      "form": {
        "education": "masters",
        "languages": [{ "code": "en", "level": "fluent" }],
        "residency": "US"
      }
    }'
  ```
</CodeGroup>

The `form` values must use the same keys the position's rules expect — `education` and language `level` from the lists above, ISO country codes for `residency`/`nationality`, and the position's `visa`/`gender` keys. Custom fields (defined via `form_fields`) can also be passed here by their `custom_<timestamp>` id; they're stored with the submission but not screened.

For a binary file (PDF/DOCX), use `job-interview-pre-screening-api-resume-binary` with the same identity fields and the file payload instead of `candidate_resume`.

### Reading the result

A completed screen returns:

<Note>
  If the candidate isn't in a screenable state, you'll still get HTTP 200 with a short `{ "decision_status": …, "message": … }` payload instead of a full result.
</Note>

## Shortcut: pre-screen + interview in one call

To screen a candidate **and** get a ready interview link back in a single request, use [`POST /job-interview-create-for-candidate-with-token`](/cookbooks/create-an-interview#option-c-one-call-per-candidate-returns-a-link) (HTTP only — not exposed as an MCP tool; call it directly with a bearer token). It resolves the position, runs pre-screening, and returns a tokenised `interview_url` plus a `status` like `ai_accept` or `recruiter_action`.

## Next steps

<CardGroup cols={2}>
  <Card title="Invite candidates" icon="user-plus" href="/cookbooks/invite-candidates">
    Invite the candidates who passed screening.
  </Card>

  <Card title="Review results" icon="clipboard-check" href="/cookbooks/review-results">
    Filter results by the `pre-screening` step to see screening outcomes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.