Skip to content

Products

HireLayer Resume Scorer

HireLayer candidate matching and ranking API: score one or many resumes against a job, criterion by criterion, with evidence, a verdict and an optional precise ranking.

The HireLayer Resume Scorer evaluates resumes against a job, criterion by criterion, and returns them best first in one POST /api/v4/resumes/score call. The job comes as a file, a text or a stored job_id; each resume as a file, a text, a parsed JSON or a stored application_id, mixed freely, up to 100 per call. Each resume gets a score from 0 to 1, a verdict, a summary and, for every criterion, a status, the evidence quoted from the resume and an explanation.

  • With files, precise_ranking, webhook_url, do_not_store_data and every other field go inside the request JSON, never as form fields: any part but request and the files is refused (400).
  • Ranking: precise_ranking: true compares the 10 best resumes of the call against each other and adds a ranking_reason to each, for 1 credit more per call.
  • Price: 1 credit per resume scored, +1 per resume file parsed. A pair already scored is free to ask again, so a recruiter can reload the ranking as often as needed. A stored job sent again with a new text or new criteria also evaluates its other stored scores again, 1 credit each; when the credits left do not cover them, the call is refused (403) before the job is changed.
  • A job text identical to the stored job returns the job as stored: title, location and language sent with it are ignored; change them with PUT /jobs/{job_id}.
  • Time: about a minute for 50 resumes. Past 10 files or 50 resumes, the call answers 202 and the scores are read at results_url or posted to a webhook. With storage, a smaller call whose evaluations still run after about 100 seconds answers 202 too.

The score is the weighted mean of the criterion scores (met 1, partial 0.67, missing 0 or 0.33 with a weak hint), criteria marked unknown left out, then × 0.75 for each required criterion clearly missing. A criterion of weight 0 is off. The verdict is an overall reading of the resume for the job:

verdictMeaning
highly_relevantShortlist.
relevantWorth a call.
marginally_relevantWeak.
not_relevantOff-topic.
Endpoint
POST /api/v4/resumes/score
Input
A job (file, text or job_id) and 1 to 100 resumes (files, texts, parsed JSON or application_ids)
Output
One score per resume, best first
Price
1 credit per resume evaluated, +1 per resume file parsed, +1 per call with precise_ranking; 0 for a pair already evaluated or a file already stored; 1 credit per other stored score evaluated again when a stored job comes with a new text or new criteria
POST/api/v4/resumes/score

Evaluates one or more resumes against a job, criterion by criterion, and returns them best first.

Synchronous or not.

The callThe answer
No webhook_url, at most 10 resume files and at most 50 resumes in all200 with every score: about a minute for 50 resumes (measured 60 to 64 seconds), less for fewer, plus the parse of any file; set a client timeout of 150 seconds. With storage, evaluations still running after about 100 seconds answer 202 instead: poll results_url
No webhook_url, more than that, with storage202; poll results_url until no score is pending
No webhook_url, more than that, without storageRefused: 400 invalid_request, param: webhook_url
With webhook_url202; the full result is posted to the URL, and with storage it can also be read at results_url
Authentication
X-API-Key header
Billing
1 credit per resume evaluated, +1 per resume file parsed, +1 per call with precise_ranking; 0 for a pair already evaluated or a file already stored; 1 credit per other stored score evaluated again when a stored job comes with a new text or new criteria
Client timeout
Client timeout ≥ 150 s

Headers

  • HireLayer-Workspacestring

    Your customer's workspace id. Case-insensitive, created on first use. Absent, calls use default.

Request body

Content type application/json, or multipart/form-data with files

  • jobobjectrequired

    Exactly one of file, text or job_id. criteria goes only with file or text; criteria alone are refused.

    ›Show 7 child fields
    • filestring

      Name of the multipart part that holds the job description file (PDF, DOCX, TXT or HTML, 4.5 MB at most). A PDF of more than 50 pages, or one without a text layer, cannot be read (422 job_unreadable).

    • job_idstring

      Alone: the id of a stored one. With file, text: your own id to store it under.

    • criteriaobject[]

      The job's own criteria; they replace the extraction.

      1–20 items

      ›Show 7 child fields
      • labelstringrequired

        1–300 characters

      • typestring
        "skill""experience""education""language""certification""location""soft_skill""other"
      • requirementstring

        required: a resume that clearly lacks it loses 25 % of its score, never eliminated outright. preferred and bonus count only through their weight.

        "required""preferred""bonus"
      • weightinteger

        Importance. 3 essential, 2 important, 1 nice to have, 0 off: kept on the job, left out of every score, of the required-criterion penalty, of required_missing and of to_verify. Texts already written (summary, explanation) stay as they are.

        0–3

      • thresholdstring | null

        Minimum level in short text, such as 3 ans minimum, Bac+2 or B2.

        ≤ 100 characters

      • criterion_idstring
      • sourcestring
    • titlestring

      ≤ 300 characters

    • locationstring

      ≤ 300 characters

    • languagestring
      "fr""en"
    • textstring

      The job description.

      1–50,000 characters

  • resumesobject[]required

    1–100 items

    ›Show 5 child fields
    • filestring

      Name of the multipart part that holds the resume file.

    • application_idstring

      Alone: the id of a stored one. With file, text, resume: your own id to store it under.

    • referencestring

      Your label, returned as is.

      ≤ 200 characters

    • textstring

      The resume as plain text.

      1–50,000 characters

    • resumeobject

      The parsed resume JSON, as POST /resumes/parse returns it in resume, or the whole response of the legacy /api/v3/parser. 1 MB at most (413), arrays and objects nested 100 levels deep at most (400). It is not parsed again: no parsing credit.

  • precise_rankingboolean

    Re-rank the 10 best resumes of this call against each other and add ranking_reason. 1 credit more for the call, when it has at least 2 resumes evaluated to rank.

    default false

  • do_not_store_databoolean

    Enterprise plan only, else 403 enterprise_plan_required: process without storing anything. See Storage.

    default false

  • webhook_urlstring (URL)

    HTTPS URL on the standard port, without credentials, that receives the result, signed.

Response

200Every resume evaluated (or failed), best score first.

  • request_idstring

    Quote it when contacting support.

  • jobobject
    ›Show 5 child fields
    • job_idstring | null

      null without storage.

    • storedboolean
    • titlestring | null
    • languagestring
      "fr""en"
    • criteriaobject[]
      ›Show 7 child fields
      • criterion_idstring

        Stable id in the job, such as crit_2. Never reused.

      • labelstring
      • typestring
        "skill""experience""education""language""certification""location""soft_skill""other"
      • requirementstring

        required: a resume that clearly lacks it loses 25 % of its score, never eliminated outright. preferred and bonus count only through their weight.

        "required""preferred""bonus"
      • weightinteger

        Importance. 3 essential, 2 important, 1 nice to have, 0 off: kept on the job, left out of every score, of the required-criterion penalty, of required_missing and of to_verify. Texts already written (summary, explanation) stay as they are.

        0–3

      • thresholdstring | null
      • sourcestring

        extracted by HireLayer, or custom: sent or edited by you.

        "extracted""custom"
  • precise_rankingboolean
  • scoresobject[]
    ›Show 15 child fields
    • rankinteger

      Place in the whole ranking, across pages: by score, except among the 10 best when precise_ranking is on. Pending and failed resumes come last.

      ≥ 1

    • application_idstring | null

      null without storage, and for a resume file that could not be read.

    • referencestring | null

      The reference the resume was sent with. Given back in the answer of POST /resumes/score, its webhook and its results_url; null in a job's stored ranking and in one stored score, which outlive the call.

    • file_namestring | null

      The name of the resume file sent to POST /resumes/score, given back as reference is.

    • evaluation_statusstring
      "completed""pending""failed"
    • staleboolean

      The resume was replaced after this evaluation. The result stays readable, and is evaluated again the next time the resume is sent to POST /resumes/score for this job.

    • scorenumber | null

      Rounded to 2 decimals; null until evaluated.

      0–1

    • verdictstring | nullmay be absent

      The overall judgment of the resume for the job: highly_relevant (shortlist), relevant (worth a call), marginally_relevant (weak), not_relevant (off-topic). Present once the evaluation is completed, absent while pending or failed. It does not follow the weights of a read, so it can disagree with score after a weight change. null when a criterion of the job was added, removed or reworded (label or threshold) since the last full evaluation of the resume, or when no overall judgment could be given. A change of weight, requirement or type leaves it as is. After a reset, it comes back when the resume is sent again to POST /resumes/score for this job: a full evaluation, 1 credit, on top of the credit the new criterion's own evaluation cost.

      "highly_relevant""relevant""marginally_relevant""not_relevant"null
    • summarystring | nullmay be absent

      Overall assessment, in the job's language. Present once the evaluation is completed. It follows verdict, written with it: null when verdict is null (a criterion of the job added, removed or reworded since the last full evaluation of the resume), back with the next full evaluation.

    • ranking_reasonstringmay be absent

      One sentence that places the resume against the others. Only with precise_ranking, for ranks 1 to 10, when at least 2 resumes are compared; absent otherwise.

    • required_missingobject[]

      Required criteria marked missing with medium or high confidence; each multiplies the score by 0.75.

      ›Show 2 child fields
      • criterion_idstring
      • labelstring
    • to_verifyobject[]

      Required criteria not clearly met, to check with the candidate; those of required_missing included.

      ›Show 2 child fields
      • criterion_idstring
      • labelstring
    • criteriaobject[]may be absent

      Per-criterion detail, in the job's criteria order, once the evaluation is completed. Always in POST /resumes/score answers; in GET /jobs/{job_id}/scores, only with include=criteria.

      ›Show 10 child fields
      • criterion_idstring
      • labelstring
      • requirementstring

        required: a resume that clearly lacks it loses 25 % of its score, never eliminated outright. preferred and bonus count only through their weight.

        "required""preferred""bonus"
      • weightinteger

        Importance. 3 essential, 2 important, 1 nice to have, 0 off: kept on the job, left out of every score, of the required-criterion penalty, of required_missing and of to_verify. Texts already written (summary, explanation) stay as they are.

        0–3

      • statusstring

        The reading of the criterion. met; partial; missing, which can still carry a weak hint (score 0.33); unknown: the resume says nothing about something resumes often leave out, such as availability, and the criterion is left out of the score.

        "met""partial""missing""unknown"
      • scorenumber | null

        1 met or exceeded, 0.67 partly met, 0.33 a weak hint, 0 absent, null unknown. The resume's score uses the exact values 2/3 and 1/3, then rounds to 2 decimals.

        00.330.671null
      • gapstring | null

        Why the evidence is partial (scores 0.67 and 0.33): the same thing, less of it; a neighbouring skill of the same trade; plausible but not shown.

        "below_threshold""related""indirect"null
      • confidencestring

        A required criterion missing with low confidence does not lower the score.

        "high""medium""low"
      • evidencestring

        Where the resume shows it: the section, then the passages quoted between quotation marks, in the resume's language. Empty when there is none. Free text, so search the quoted passages rather than the whole string.

      • explanationstring

        One sentence, in the job's language.

    • errorobject | null

      Why this resume failed, when evaluation_status is failed. It was not billed.

      ›Show 4 child fields
      • codestring

        Stable codes: branch on them, never on message. method_not_allowed answers a method a path does not take (405, with an Allow header), outside any operation.

        "invalid_request""storage_required""missing_api_key""invalid_api_key""insufficient_credits""enterprise_plan_required""not_found""payload_too_large""resume_unreadable""job_unreadable""rate_limited""model_unavailable""internal_error""method_not_allowed"
      • messagestring

        Readable explanation, for logs.

      • paramstringmay be absent

        The field or parameter at fault, when there is one, such as job.criteria or resumes[2].file.

      • doc_urlstring (URL)
    • evaluated_atstring (date-time) | null
  • credits_usedinteger

    Credits this call cost.

    ≥ 0

202Accepted, with webhook_url, beyond the synchronous limit, or (with storage) when the evaluations outlast about 100 seconds. The result is posted to webhook_url when there is one and, with storage, readable at results_url.

  • statusstring
    "pending"
  • request_idstring

    Identifies this call in results_url and in the webhook.

  • job_idstring | null
  • resumes_countinteger

    ≥ 1

  • webhook_urlstring (URL) | null
  • results_urlstring (URL) | null

    With storage: GET /jobs/{job_id}/scores?request_id=…, the resumes of this call only. null without storage.

Errors

  • 400
    invalid_requestjob.file, job.text or job.job_id is required.

    param: job. No job, or a job with none of file, text and job_id; or both file and text.

    Fix, then retry

  • 400
    invalid_requestjob.criteria needs job.text or job.file: a stored job_id keeps its own criteria.

    param: job.criteria. job.criteria (or title, location, language) with a stored job_id alone.

    Fix, then retry

  • 400
    invalid_requestjob.criteria[0].weight must be 0, 1, 2 or 3.

    param: job.criteria[0].weight. A criterion breaks a rule (here its weight): param names the field.

    Fix, then retry

  • 400
    invalid_requestresumes must be a non-empty array.

    param: resumes. No resume, or more than 100.

    Fix, then retry

  • 400
    invalid_requestresumes[0] needs file, text, resume or application_id.

    param: resumes[0]. A resume with none of file, text, resume and application_id, or with several of the first three.

    Fix, then retry

  • 400
    invalid_requestresumes[0].file names no part of the call.

    param: resumes[0].file. A file (or job.file) that names no part of the call.

    Fix, then retry

  • 400
    invalid_requestresumes[1] is the same resume as resumes[0] (cand-42): send each resume once.

    param: resumes[1].application_id. The same resume twice in the call.

    Fix, then retry

  • 400
    invalid_requestresumes[0].application_id cannot be score: that name is reserved for POST /api/v4/resumes/score.

    param: resumes[0].application_id. An application_id that is not a valid id, or a name the paths reserve.

    Fix, then retry

  • 400
    invalid_requestprecise_ranking is a form field: every field of the call goes in the request part's JSON, the other parts are files.

    param: precise_ranking. In multipart, a field sent as its own part instead of inside request.

    Fix, then retry

  • 400
    invalid_requestwebhook_url is required without storage past 10 resume files or 50 resumes: the result is posted there, as nothing is kept to read.

    param: webhook_url. A call without storage past the synchronous limit, without webhook_url.

    Fix, then retry

  • 400
    invalid_requestThe request body is not valid JSON.

    The body is not valid JSON, not an object, or not sent as application/json.

    Fix, then retry

  • 400
    invalid_requestHireLayer-Workspace must contain letters, digits and . _ : ~ -, up to 200 characters.

    param: HireLayer-Workspace. HireLayer-Workspace is not a valid id.

    Fix, then retry

  • 400
    storage_requiredjob.job_id needs storage: send the job as text with do_not_store_data.

    param: job.job_id. A stored job_id without storage.

    Do not retry

  • 400
    storage_requiredresumes[0].application_id needs storage: send the resume as a file, a text or its JSON.

    param: resumes[0].application_id. A stored application_id alone without storage.

    Do not retry

  • 401
    missing_api_keyThe X-API-Key header is missing.

    No X-API-Key header.

    Fix, then retry

  • 401
    missing_api_keyAuthorization: Bearer is not accepted: send the key in the X-API-Key header.

    The key sent as Authorization: Bearer: it goes in X-API-Key.

    Fix, then retry

  • 401
    missing_api_keyThe key goes in the X-API-Key header, not in the URL.

    The key sent in the URL: it goes in X-API-Key.

    Fix, then retry

  • 401
    invalid_api_keyInvalid API key: it is unknown, malformed or revoked.

    The key is unknown, malformed or revoked.

    Fix, then retry

  • 403
    insufficient_creditsThis call scores 38 new pairs and parses 2 files, 1 credit each: 40 credits, and 32 are left. Nothing was processed or billed.

    The credits left do not cover the call: checked before anything is parsed, stored or evaluated.

    Fix, then retry

  • 403
    enterprise_plan_requireddo_not_store_data is available on the Enterprise plan only. Nothing was processed or billed.

    param: do_not_store_data. do_not_store_data: true below the Enterprise plan.

    Do not retry

  • 404
    not_foundNo job req-7 in workspace default.

    param: job.job_id. No job under job.job_id in the workspace. An unknown application_id fails only its own score.

    Do not retry

  • 413
    payload_too_largeresumes[0].file must be 4.5 MB or less.

    param: resumes[0].file. A file over 4.5 MB (job.file too).

    Fix, then retry

  • 413
    payload_too_largeresumes[0].text must be 50,000 characters or less.

    param: resumes[0].text. A resume or job text over 50,000 characters (job.text too).

    Fix, then retry

  • 413
    payload_too_largeresumes[0].resume must be 1 MB or less.

    param: resumes[0].resume. A parsed resume over 1 MB.

    Fix, then retry

  • 413
    payload_too_largeThe files of a call must be 50 MB or less in total.

    The files of the call are over 50 MB together.

    Fix, then retry

  • 422
    job_unreadablejob.file has no text layer: it is probably a scan. Send its text instead.

    param: job.file. The job file cannot be read. An unreadable resume file fails only its own score.

    Do not retry

  • 429
    rate_limitedToo many files are being parsed at once for this account: retry after the Retry-After delay.

    Too many parses at once. Header Retry-After: <seconds>.

    Retry with backoff

  • 500
    internal_errorInternal error: retry later. If it persists, contact support with the request_id.

    An unexpected error.

    Retry with backoff

  • 502
    model_unavailableThe analysis did not complete after several attempts. Nothing was stored or billed: retry with backoff.

    The analysis did not complete.

    Retry with backoff

  • 502
    internal_errorThe service could not be reached: retry later.

    The service could not be reached.

    Retry with backoff

  • 503
    internal_errorThe credit service is briefly unavailable: retry in a second.

    The credit service is briefly unavailable. Header Retry-After: <seconds>.

    Retry with backoff

  • 504
    internal_errorThe service did not answer in time. The change may still be applied: read it before sending it again.

    No answer in time.

    Retry with backoff

Behavior

Inputs, mixed freely in one call
The job comes as a file, a text or a stored job_id, alone or (file or text only) with its criteria in the format POST /jobs/extract-criteria returns. Criteria without the job are refused. Each resume comes as a file, a text, the parsed resume JSON ({"resume": {…}}, no parsing credit) or a stored application_id, including one parsed by the legacy /api/v3/parser. A job or resume sent as content can carry your own job_id or application_id.
One request shape
Without files, send ScoreRequest as a JSON body. With files, send multipart/form-data: the same JSON in the request part, and each file in a part whose name the JSON cites ({"file": "cv1"} reads the part cv1). A cited part that is missing is refused (400, param naming it). Every other field, do_not_store_data and webhook_url included, goes in the request JSON, not in a form field of its own: a text part other than request is refused (400, param naming it), unlike POST /resumes/parse, which takes its fields as form fields.
Limits
at most 100 resumes per call; each file 4.5 MB at most, 50 MB of files per call. To score more files, parse them first with POST /resumes/parse and send their ids, or split the call. Without storage, calls of at most 10 files are fully synchronous and keep nothing, not even an undelivered webhook result. A text over 50,000 characters or a parsed resume JSON over 1 MB refuses the call (413) before any work; a resume file or JSON whose text is longer is scored on its first 50,000 characters.
Each resume once
with storage, two resumes of a call stored under the same id (the same file, text or parsed JSON sent twice without application_id, or the same application_id) refuse the call (400 invalid_request, param naming the second, such as resumes[3]) before any file is parsed. Without storage, nothing is stored under an id: both are evaluated and billed.
Unknown ids
an unknown job_id refuses the call (404). An unknown application_id fails only that resume (evaluation_status: failed, error.code: not_found), unbilled.
Credits
1 per resume evaluated, plus 1 per resume file parsed. A pair already evaluated costs 0, unless it is stale (the resume was replaced) or its verdict was reset by a change of the job's criteria: then it is evaluated again in full, 1 credit. A file already stored costs 0. precise_ranking costs 1 credit more per call, when the call has at least 2 resumes evaluated to rank. A resume that fails (unreadable file) is not billed and does not fail the call; it comes last, with evaluation_status: failed and error. A stored job sent again with a new text or new criteria also evaluates its other stored scores again, 1 credit each, not counted in credits_used (see "A job sent again"); a call whose credits do not cover them is refused (403) before the job is changed. That check asks for the worst case: when the call sends the stored job with a new text or without one of its stored criteria, every stored score of the job counts as evaluated again. The call then bills only what is actually evaluated, which can be less.
Score
weighted mean of the criterion scores, criteria marked unknown left out, then × 0.75 for each required criterion marked missing with medium or high confidence. A criterion of weight 0 is off: left out of the mean, of the penalty and of to_verify.
A job sent again
a job text or file whose id is already stored (the same text, or your job_id) updates the stored job. Criteria sent with it replace the stored ones; a changed text without criteria has its criteria extracted again; the same text without criteria returns the stored job as it is, and the call's title, location and language are then ignored: change them with PUT /jobs/{job_id}. As with PUT /jobs/{job_id}, new criteria also evaluate the job's other stored scores again in the background, 1 credit each, not counted in credits_used.
Criterion ids
criteria of a new job are numbered crit_1, crit_2… in their order. When criteria replace those of a stored job, one with the same label and threshold as a stored criterion keeps its id and its results; the others take the next unused numbers. An id is never reused.
Weights
scoring uses the job's stored weights. To change one for good, PATCH /jobs/{job_id}/criteria/{criterion_id}; to try one for a single view, GET /jobs/{job_id}/scores?weights[crit_3]=0. Both re-rank at once, for free.
Without storage
nothing is kept (see DoNotStoreData); job.job_id and each application_id are null, so give every resume a reference to match the results to your inputs; every call evaluates every resume again and bills it (send a resume parsed once as its JSON, {"resume": {…}}, so as not to pay its parsing again); results_url is null. Criteria sent back with the job keep their order, so a ranking can be recomputed on your side from criteria[].score and the weights. A call with webhook_url answers 202 at once: its files are read after it, then the job and the resumes wait until they are evaluated, and the result until it is delivered; each is deleted then, 24 hours at most after the call. A synchronous call without storage that fails (5xx) bills nothing, not even the files it parsed: send it again as it is.
# Example ids: replace job_30cfa454c97663f3e1801e5a61b87d74 with a job_id returned by POST /api/v4/jobs/extract-criteria, and file_2c982f413a5e9e79563f8a1fcb5c7caa, file_addd6bc79e818deebe289d009058d6b1 with application_ids returned by POST /api/v4/resumes/parse.
curl -X POST https://hirelayer.co/api/v4/resumes/score \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job": {
      "job_id": "job_30cfa454c97663f3e1801e5a61b87d74"
    },
    "resumes": [
      {
        "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa"
      },
      {
        "application_id": "file_addd6bc79e818deebe289d009058d6b1"
      }
    ],
    "precise_ranking": true
  }'
{
  "request_id": "5d0f3c2e-8a41-4b6e-9f27-1c3b7a9e4d10",
  "job": {
    "job_id": "job_30cfa454c97663f3e1801e5a61b87d74",
    "stored": true,
    "title": "Ingénieur IA / Développeur backend Node.js (H/F)",
    "language": "fr",
    "criteria": [
      {
        "criterion_id": "crit_1",
        "label": "Développement backend Node.js / TypeScript",
        "type": "skill",
        "requirement": "preferred",
        "weight": 3,
        "threshold": "3 ans minimum",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_2",
        "label": "Intégration de LLM en production",
        "type": "skill",
        "requirement": "required",
        "weight": 3,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_3",
        "label": "AWS (Lambda, S3, DynamoDB)",
        "type": "skill",
        "requirement": "preferred",
        "weight": 2,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_4",
        "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
        "type": "education",
        "requirement": "preferred",
        "weight": 2,
        "threshold": "Bac+5",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_5",
        "label": "Anglais",
        "type": "language",
        "requirement": "required",
        "weight": 2,
        "threshold": "B2 minimum",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_6",
        "label": "Encadrement technique",
        "type": "experience",
        "requirement": "bonus",
        "weight": 1,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_7",
        "label": "Certification AWS",
        "type": "certification",
        "requirement": "bonus",
        "weight": 1,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_8",
        "label": "Astreinte",
        "type": "other",
        "requirement": "required",
        "weight": 1,
        "threshold": "1 semaine sur 6",
        "source": "extracted"
      }
    ]
  },
  "precise_ranking": true,
  "scores": [
    {
      "rank": 1,
      "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
      "reference": "cand-paul",
      "file_name": "cv-paul-verse.pdf",
      "evaluation_status": "completed",
      "stale": false,
      "score": 0.9,
      "verdict": "highly_relevant",
      "summary": "Candidat très pertinent avec une expertise forte en LLM et AWS, un excellent niveau d'anglais, à présélectionner malgré une expérience en Node.js légèrement sous le seuil.",
      "ranking_reason": "Le seul des deux à avoir intégré des LLM en production ; son expérience Node.js reste un peu sous le seuil.",
      "required_missing": [],
      "to_verify": [
        {
          "criterion_id": "crit_8",
          "label": "Astreinte"
        }
      ],
      "criteria": [
        {
          "criterion_id": "crit_1",
          "label": "Développement backend Node.js / TypeScript",
          "requirement": "preferred",
          "weight": 3,
          "status": "partial",
          "score": 0.67,
          "gap": "below_threshold",
          "confidence": "medium",
          "evidence": "Ingénieur IA - Novalys AI (2024-25) : 'Stack API (serverless AWS) : API Gateway, Lambda, S3, DynamoDB, Terraform, Node.js' ; Data Scientist - Allobrain (2023-24) : 'Amélioration de compétences en Python et JavaScript'",
          "explanation": "Environ 2 ans d'expérience en Node.js clairement identifiés (Novalys AI), en dessous du seuil de 3 ans demandé, avec une expérience complémentaire en Python/JavaScript chez Allobrain."
        },
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production",
          "requirement": "required",
          "weight": 3,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Ingénieur IA - Novalys AI : 'Développement d’une plateforme SaaS intégrant plusieurs fonctionnalités basées sur l’IA générative', 'orchestration des modèles', 'Solution scalable, 500k+ documents traités' ; Data Scientist - Allobrain : 'focus sur le NLP, les LLM'",
          "explanation": "Le candidat démontre une intégration de LLM en production à grande échelle (500k+ documents, plateforme SaaS) qui dépasse l'exigence."
        },
        {
          "criterion_id": "crit_3",
          "label": "AWS (Lambda, S3, DynamoDB)",
          "requirement": "preferred",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Ingénieur IA - Novalys AI : 'Stack API (serverless AWS) : API Gateway, Lambda, S3, DynamoDB, Terraform'",
          "explanation": "Les services AWS Lambda, S3 et DynamoDB sont explicitement utilisés dans un contexte professionnel récent."
        },
        {
          "criterion_id": "crit_4",
          "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
          "requirement": "preferred",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "EFREI - INGÉNIEUR - BIG DATA & IA : 'Master 2 en Ecole d’ingénieur du numérique'",
          "explanation": "Le candidat possède un Master 2 en école d'ingénieur, conforme à l'exigence de diplôme."
        },
        {
          "criterion_id": "crit_5",
          "label": "Anglais",
          "requirement": "required",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Anglais : TOEIC 980/990 ; EFREI : 'cours intégralement en Anglais'",
          "explanation": "Le score TOEIC de 980/990 et la formation en anglais attestent d'un niveau bien supérieur au B2 requis."
        },
        {
          "criterion_id": "crit_6",
          "label": "Encadrement technique",
          "requirement": "bonus",
          "weight": 1,
          "status": "partial",
          "score": 0.67,
          "gap": "related",
          "confidence": "medium",
          "evidence": "COP1 - DIRECTEUR DU RÉSEAU : 'Gouvernance et direction bénévole', '25 salariés', 'gestion des bénévoles : de 3 000 à 5 000 membres'",
          "explanation": "Expérience significative en gestion d'équipe et de salariés (COP1), mais dans un contexte associatif/bénévole plutôt que technique."
        },
        {
          "criterion_id": "crit_7",
          "label": "Certification AWS",
          "requirement": "bonus",
          "weight": 1,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "AWS - CLOUD PRACTITIONER",
          "explanation": "Le candidat détient la certification AWS Cloud Practitioner, ce qui correspond au critère 'un plus'."
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte",
          "requirement": "required",
          "weight": 1,
          "status": "unknown",
          "score": null,
          "gap": null,
          "confidence": "low",
          "evidence": "",
          "explanation": "L'acceptation de l'astreinte n'est pas mentionnée sur le CV."
        }
      ],
      "error": null,
      "evaluated_at": "2026-10-26T22:39:25Z"
    },
    {
      "rank": 2,
      "application_id": "file_addd6bc79e818deebe289d009058d6b1",
      "reference": "cand-rafael",
      "file_name": "cv-rafael-costa.pdf",
      "evaluation_status": "completed",
      "stale": false,
      "score": 0.25,
      "verdict": "marginally_relevant",
      "summary": "Candidat exclusivement frontend (React/TypeScript) sans expérience backend Node.js ni intégration de LLM, deux critères essentiels non satisfaits.",
      "ranking_reason": "Profil frontend sans LLM ni AWS : loin derrière le premier sur les deux critères centraux.",
      "required_missing": [
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production"
        }
      ],
      "to_verify": [
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production"
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte"
        }
      ],
      "criteria": [
        {
          "criterion_id": "crit_1",
          "label": "Développement backend Node.js / TypeScript",
          "requirement": "preferred",
          "weight": 3,
          "status": "missing",
          "score": 0.33,
          "gap": "related",
          "confidence": "high",
          "evidence": "\"Frontend engineer with 7+ years building React and TypeScript products\"",
          "explanation": "Le candidat a 7 ans d'expérience en TypeScript, mais exclusivement en frontend React ; Node.js est seulement listé dans la tech stack sans expérience associée, ce qui est insuffisant pour un poste backend."
        },
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production",
          "requirement": "required",
          "weight": 3,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune mention d'intégration de LLM, de RAG ou d'IA dans le CV ; ce critère obligatoire n'est pas satisfait."
        },
        {
          "criterion_id": "crit_3",
          "label": "AWS (Lambda, S3, DynamoDB)",
          "requirement": "preferred",
          "weight": 2,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune mention d'AWS, Lambda, S3 ou DynamoDB ; le candidat évolue sur un périmètre frontend sans exposition à l'infrastructure cloud."
        },
        {
          "criterion_id": "crit_4",
          "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
          "requirement": "preferred",
          "weight": 2,
          "status": "partial",
          "score": 0.67,
          "gap": "below_threshold",
          "confidence": "medium",
          "evidence": "\"Bacharelado em Ciência da Computação (Bachelor of Computer Science)\"",
          "explanation": "Le candidat possède un Bachelor en informatique (4 ans au Brésil), niveau Bac+4 estimé, en dessous du Bac+5 requis."
        },
        {
          "criterion_id": "crit_5",
          "label": "Anglais",
          "requirement": "required",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "\"English (C1)\"",
          "explanation": "Niveau C1 en anglais, supérieur au seuil B2 obligatoire, avec 5 ans d'expérience en Irlande."
        },
        {
          "criterion_id": "crit_6",
          "label": "Encadrement technique",
          "requirement": "bonus",
          "weight": 1,
          "status": "missing",
          "score": 0.33,
          "gap": "indirect",
          "confidence": "medium",
          "evidence": "\"Co-own the design system (Storybook, Tailwind CSS, Radix UI) with two designers\"",
          "explanation": "Pas d'encadrement formel d'équipe technique ; seule une co-responsabilité sur un design system avec des designers est mentionnée."
        },
        {
          "criterion_id": "crit_7",
          "label": "Certification AWS",
          "requirement": "bonus",
          "weight": 1,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune certification AWS ou autre certification technique mentionnée."
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte",
          "requirement": "required",
          "weight": 1,
          "status": "unknown",
          "score": null,
          "gap": null,
          "confidence": "low",
          "evidence": "",
          "explanation": "L'acceptation de l'astreinte n'est pas renseignée sur le CV."
        }
      ],
      "error": null,
      "evaluated_at": "2026-10-26T22:40:51Z"
    }
  ],
  "credits_used": 4
}

With files. Resume files and a job text in one call: the JSON goes in the request part, and each file in the part its file names. Each file is parsed and stored, +1 credit.

curl -X POST https://hirelayer.co/api/v4/resumes/score \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  --form-string 'request={"job":{"text":"Ingénieur IA / Développeur backend Node.js (H/F)\nLumio · Paris 9e · CDI · Hybride (2 jours de télétravail par semaine)\n- 3 ans minimum d'\''expérience en développement backend Node.js / TypeScript\n- Expérience indispensable de l'\''intégration de LLM en production\n- Vous maîtrisez AWS (Lambda, S3, DynamoDB) ; une certification AWS est un plus\n- Anglais courant (B2 minimum) obligatoire"},"resumes":[{"file":"cv1","reference":"cand-paul"},{"file":"cv2","reference":"cand-rafael"}],"precise_ranking":true}' \
  -F "cv1=@cv-paul-verse.pdf;type=application/pdf" \
  -F "cv2=@cv-rafael-costa.pdf;type=application/pdf"
{
  "request_id": "5d0f3c2e-8a41-4b6e-9f27-1c3b7a9e4d10",
  "job": {
    "job_id": "job_30cfa454c97663f3e1801e5a61b87d74",
    "stored": true,
    "title": "Ingénieur IA / Développeur backend Node.js (H/F)",
    "language": "fr",
    "criteria": [
      {
        "criterion_id": "crit_1",
        "label": "Développement backend Node.js / TypeScript",
        "type": "skill",
        "requirement": "preferred",
        "weight": 3,
        "threshold": "3 ans minimum",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_2",
        "label": "Intégration de LLM en production",
        "type": "skill",
        "requirement": "required",
        "weight": 3,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_3",
        "label": "AWS (Lambda, S3, DynamoDB)",
        "type": "skill",
        "requirement": "preferred",
        "weight": 2,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_4",
        "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
        "type": "education",
        "requirement": "preferred",
        "weight": 2,
        "threshold": "Bac+5",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_5",
        "label": "Anglais",
        "type": "language",
        "requirement": "required",
        "weight": 2,
        "threshold": "B2 minimum",
        "source": "extracted"
      },
      {
        "criterion_id": "crit_6",
        "label": "Encadrement technique",
        "type": "experience",
        "requirement": "bonus",
        "weight": 1,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_7",
        "label": "Certification AWS",
        "type": "certification",
        "requirement": "bonus",
        "weight": 1,
        "threshold": null,
        "source": "extracted"
      },
      {
        "criterion_id": "crit_8",
        "label": "Astreinte",
        "type": "other",
        "requirement": "required",
        "weight": 1,
        "threshold": "1 semaine sur 6",
        "source": "extracted"
      }
    ]
  },
  "precise_ranking": true,
  "scores": [
    {
      "rank": 1,
      "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa",
      "reference": "cand-paul",
      "file_name": "cv-paul-verse.pdf",
      "evaluation_status": "completed",
      "stale": false,
      "score": 0.9,
      "verdict": "highly_relevant",
      "summary": "Candidat très pertinent avec une expertise forte en LLM et AWS, un excellent niveau d'anglais, à présélectionner malgré une expérience en Node.js légèrement sous le seuil.",
      "ranking_reason": "Le seul des deux à avoir intégré des LLM en production ; son expérience Node.js reste un peu sous le seuil.",
      "required_missing": [],
      "to_verify": [
        {
          "criterion_id": "crit_8",
          "label": "Astreinte"
        }
      ],
      "criteria": [
        {
          "criterion_id": "crit_1",
          "label": "Développement backend Node.js / TypeScript",
          "requirement": "preferred",
          "weight": 3,
          "status": "partial",
          "score": 0.67,
          "gap": "below_threshold",
          "confidence": "medium",
          "evidence": "Ingénieur IA - Novalys AI (2024-25) : 'Stack API (serverless AWS) : API Gateway, Lambda, S3, DynamoDB, Terraform, Node.js' ; Data Scientist - Allobrain (2023-24) : 'Amélioration de compétences en Python et JavaScript'",
          "explanation": "Environ 2 ans d'expérience en Node.js clairement identifiés (Novalys AI), en dessous du seuil de 3 ans demandé, avec une expérience complémentaire en Python/JavaScript chez Allobrain."
        },
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production",
          "requirement": "required",
          "weight": 3,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Ingénieur IA - Novalys AI : 'Développement d’une plateforme SaaS intégrant plusieurs fonctionnalités basées sur l’IA générative', 'orchestration des modèles', 'Solution scalable, 500k+ documents traités' ; Data Scientist - Allobrain : 'focus sur le NLP, les LLM'",
          "explanation": "Le candidat démontre une intégration de LLM en production à grande échelle (500k+ documents, plateforme SaaS) qui dépasse l'exigence."
        },
        {
          "criterion_id": "crit_3",
          "label": "AWS (Lambda, S3, DynamoDB)",
          "requirement": "preferred",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Ingénieur IA - Novalys AI : 'Stack API (serverless AWS) : API Gateway, Lambda, S3, DynamoDB, Terraform'",
          "explanation": "Les services AWS Lambda, S3 et DynamoDB sont explicitement utilisés dans un contexte professionnel récent."
        },
        {
          "criterion_id": "crit_4",
          "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
          "requirement": "preferred",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "EFREI - INGÉNIEUR - BIG DATA & IA : 'Master 2 en Ecole d’ingénieur du numérique'",
          "explanation": "Le candidat possède un Master 2 en école d'ingénieur, conforme à l'exigence de diplôme."
        },
        {
          "criterion_id": "crit_5",
          "label": "Anglais",
          "requirement": "required",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "Anglais : TOEIC 980/990 ; EFREI : 'cours intégralement en Anglais'",
          "explanation": "Le score TOEIC de 980/990 et la formation en anglais attestent d'un niveau bien supérieur au B2 requis."
        },
        {
          "criterion_id": "crit_6",
          "label": "Encadrement technique",
          "requirement": "bonus",
          "weight": 1,
          "status": "partial",
          "score": 0.67,
          "gap": "related",
          "confidence": "medium",
          "evidence": "COP1 - DIRECTEUR DU RÉSEAU : 'Gouvernance et direction bénévole', '25 salariés', 'gestion des bénévoles : de 3 000 à 5 000 membres'",
          "explanation": "Expérience significative en gestion d'équipe et de salariés (COP1), mais dans un contexte associatif/bénévole plutôt que technique."
        },
        {
          "criterion_id": "crit_7",
          "label": "Certification AWS",
          "requirement": "bonus",
          "weight": 1,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "AWS - CLOUD PRACTITIONER",
          "explanation": "Le candidat détient la certification AWS Cloud Practitioner, ce qui correspond au critère 'un plus'."
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte",
          "requirement": "required",
          "weight": 1,
          "status": "unknown",
          "score": null,
          "gap": null,
          "confidence": "low",
          "evidence": "",
          "explanation": "L'acceptation de l'astreinte n'est pas mentionnée sur le CV."
        }
      ],
      "error": null,
      "evaluated_at": "2026-10-26T22:39:25Z"
    },
    {
      "rank": 2,
      "application_id": "file_addd6bc79e818deebe289d009058d6b1",
      "reference": "cand-rafael",
      "file_name": "cv-rafael-costa.pdf",
      "evaluation_status": "completed",
      "stale": false,
      "score": 0.25,
      "verdict": "marginally_relevant",
      "summary": "Candidat exclusivement frontend (React/TypeScript) sans expérience backend Node.js ni intégration de LLM, deux critères essentiels non satisfaits.",
      "ranking_reason": "Profil frontend sans LLM ni AWS : loin derrière le premier sur les deux critères centraux.",
      "required_missing": [
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production"
        }
      ],
      "to_verify": [
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production"
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte"
        }
      ],
      "criteria": [
        {
          "criterion_id": "crit_1",
          "label": "Développement backend Node.js / TypeScript",
          "requirement": "preferred",
          "weight": 3,
          "status": "missing",
          "score": 0.33,
          "gap": "related",
          "confidence": "high",
          "evidence": "\"Frontend engineer with 7+ years building React and TypeScript products\"",
          "explanation": "Le candidat a 7 ans d'expérience en TypeScript, mais exclusivement en frontend React ; Node.js est seulement listé dans la tech stack sans expérience associée, ce qui est insuffisant pour un poste backend."
        },
        {
          "criterion_id": "crit_2",
          "label": "Intégration de LLM en production",
          "requirement": "required",
          "weight": 3,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune mention d'intégration de LLM, de RAG ou d'IA dans le CV ; ce critère obligatoire n'est pas satisfait."
        },
        {
          "criterion_id": "crit_3",
          "label": "AWS (Lambda, S3, DynamoDB)",
          "requirement": "preferred",
          "weight": 2,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune mention d'AWS, Lambda, S3 ou DynamoDB ; le candidat évolue sur un périmètre frontend sans exposition à l'infrastructure cloud."
        },
        {
          "criterion_id": "crit_4",
          "label": "Diplôme d'ingénieur ou Bac+5 en informatique",
          "requirement": "preferred",
          "weight": 2,
          "status": "partial",
          "score": 0.67,
          "gap": "below_threshold",
          "confidence": "medium",
          "evidence": "\"Bacharelado em Ciência da Computação (Bachelor of Computer Science)\"",
          "explanation": "Le candidat possède un Bachelor en informatique (4 ans au Brésil), niveau Bac+4 estimé, en dessous du Bac+5 requis."
        },
        {
          "criterion_id": "crit_5",
          "label": "Anglais",
          "requirement": "required",
          "weight": 2,
          "status": "met",
          "score": 1,
          "gap": null,
          "confidence": "high",
          "evidence": "\"English (C1)\"",
          "explanation": "Niveau C1 en anglais, supérieur au seuil B2 obligatoire, avec 5 ans d'expérience en Irlande."
        },
        {
          "criterion_id": "crit_6",
          "label": "Encadrement technique",
          "requirement": "bonus",
          "weight": 1,
          "status": "missing",
          "score": 0.33,
          "gap": "indirect",
          "confidence": "medium",
          "evidence": "\"Co-own the design system (Storybook, Tailwind CSS, Radix UI) with two designers\"",
          "explanation": "Pas d'encadrement formel d'équipe technique ; seule une co-responsabilité sur un design system avec des designers est mentionnée."
        },
        {
          "criterion_id": "crit_7",
          "label": "Certification AWS",
          "requirement": "bonus",
          "weight": 1,
          "status": "missing",
          "score": 0,
          "gap": null,
          "confidence": "high",
          "evidence": "",
          "explanation": "Aucune certification AWS ou autre certification technique mentionnée."
        },
        {
          "criterion_id": "crit_8",
          "label": "Astreinte",
          "requirement": "required",
          "weight": 1,
          "status": "unknown",
          "score": null,
          "gap": null,
          "confidence": "low",
          "evidence": "",
          "explanation": "L'acceptation de l'astreinte n'est pas renseignée sur le CV."
        }
      ],
      "error": null,
      "evaluated_at": "2026-10-26T22:40:51Z"
    }
  ],
  "credits_used": 4
}

With a webhook. With webhook_url, the call answers 202 at once and the scores are posted to that URL, signed: see Webhooks. With storage, results_url reads them too.

# Example ids: replace job_30cfa454c97663f3e1801e5a61b87d74 with a job_id returned by POST /api/v4/jobs/extract-criteria, and file_2c982f413a5e9e79563f8a1fcb5c7caa with an application_id returned by POST /api/v4/resumes/parse.
curl -X POST https://hirelayer.co/api/v4/resumes/score \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job": {
      "job_id": "job_30cfa454c97663f3e1801e5a61b87d74"
    },
    "resumes": [
      {
        "application_id": "file_2c982f413a5e9e79563f8a1fcb5c7caa"
      }
    ],
    "webhook_url": "https://example.com/webhooks/hirelayer"
  }'
{
  "status": "pending",
  "request_id": "5d0a3c1e-2f4b-4c8e-9a61-0b7e2d9f4c13",
  "job_id": "job_30cfa454c97663f3e1801e5a61b87d74",
  "resumes_count": 1,
  "webhook_url": "https://example.com/webhooks/hirelayer",
  "results_url": "https://hirelayer.co/api/v4/jobs/job_30cfa454c97663f3e1801e5a61b87d74/scores?request_id=5d0a3c1e-2f4b-4c8e-9a61-0b7e2d9f4c13"
}

With storage, every score of a job can be read again, best first, with other weights for one view: GET /jobs/{job_id}/scores?weights[crit_3]=0 (free, nothing is evaluated again; see Jobs and scores). To change a weight for good, PATCH /jobs/{job_id}/criteria/{criterion_id}.