Guides

Build a screening pipeline

Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result.

Each API works on its own. Together they cover a screening workflow: structure the job once, parse every resume, shortlist, then explain.

  1. 1

    Extract the job criteria once

    Call Job Extract when a job is created or its description changes. Let a recruiter review the criteria, then store them with the job: Match takes them as they are.

  2. 2

    Parse each resume

    Call CV Extract when a resume arrives and store the JSON. Keep info_resume.text for matching, truncated to 50,000 characters.

  3. 3

    Rank the shortlist

    Rank orders up to 10 candidates for one job in a single call. For larger pools, pre-filter first, or score each candidate with Match.

  4. 4

    Explain with Match

    Match evaluates a candidate against every criterion. The score ignores is_mandatory: check mandatory criteria with a not_valid status in your own code.

Save the client as hirelayer.py or hirelayer.mts, put the job description in job.txt, then run python screening.py or npx tsx screening.mts. Credits used: 1 for the criteria, 1 per resume, 1 for the ranking and 1 for the explanation.

# hirelayer.py — minimal client with retries. Requires: pip install requests
import mimetypes
import os
import random
import time

import requests

API_BASE = "https://onlineresumeparser.com/api"


class HireLayerError(Exception):
    def __init__(self, status, message, code=None, request_id=None):
        super().__init__(f"HireLayer {status}: {message}")
        self.status = status
        self.code = code
        self.request_id = request_id


def _delay(attempt, retry_after):
    if retry_after and retry_after.isdigit():
        return int(retry_after)
    return min(30, 2**attempt) + random.random()


def call(method, path, *, json=None, files=None, data=None, timeout=65, max_retries=3):
    """Call HireLayer. Retries 5xx and connection failures, raises HireLayerError otherwise."""
    headers = {"X-API-Key": os.environ["HIRELAYER_API_KEY"]}
    for attempt in range(max_retries + 1):
        try:
            response = requests.request(
                method,
                API_BASE + path,
                headers=headers,
                json=json,
                files=files,
                data=data,
                timeout=timeout,
            )
        except requests.ConnectionError:
            if attempt == max_retries:
                raise
            time.sleep(_delay(attempt, None))
            continue

        if response.ok:
            return response.json()
        if response.status_code >= 500 and attempt < max_retries:
            time.sleep(_delay(attempt, response.headers.get("Retry-After")))
            continue

        try:
            body = response.json()
        except ValueError:
            body = {}
        raise HireLayerError(
            response.status_code,
            body.get("error", response.reason),
            body.get("code"),
            response.headers.get("x-parser-request-id"),
        )


def parse_resume(path, application_id=None):
    content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
    with open(path, "rb") as file:
        content = file.read()  # bytes can be re-sent on retry
    return call(
        "POST",
        "/v3/parser",
        files={"file": (os.path.basename(path), content, content_type)},
        data={"application_id": application_id} if application_id else None,
        timeout=150,
    )
# screening.py — parse resumes, extract criteria, rank, then explain the top match.
from hirelayer import call, parse_resume

MAX_TEXT = 50_000  # Match and Rank limit; resume text can reach 100,000 characters

with open("job.txt", encoding="utf-8") as file:
    job_text = file.read()

# 1. Extract criteria once per job, then store them with the job.
criteria = call("POST", "/v1/jobs/extract-criteria", json={"job_text": job_text})[
    "matching_criteria"
]

# 2. Parse each resume (synchronous, about 35 seconds each).
resumes = {}
for path in ["alex.pdf", "sam.pdf", "charlie.pdf"]:
    resume = parse_resume(path, application_id=path)
    resumes[path] = resume["info_resume"]["text"][:MAX_TEXT]

# 3. Rank up to 10 candidates in one call.
rankings = call(
    "POST",
    "/v1/matching/job-candidates/rank",
    json={
        "job_text": job_text,
        "candidates": [
            {"id": key, "candidate_text": text} for key, text in resumes.items()
        ],
    },
)["rankings"]

# 4. Explain the best candidate, criterion by criterion.
best = rankings[0]["candidate_id"]
match = call(
    "POST",
    "/v1/matching/job-candidate",
    json={
        "job_text": job_text,
        "candidate_text": resumes[best],
        "matching_criteria": criteria,
    },
)

print(best, round(match["score"], 2), match["summary"])
for criterion in match["evaluated_criteria"]:
    if criterion["is_mandatory"] and criterion["match_status"] == "not_valid":
        print("Missing mandatory criterion:", criterion["label"])
  • Call HireLayer from a background job, not from a user-facing request: parsing takes about 35 seconds.
  • Store results: criteria with the job, parsed JSON with the candidate. Re-running a call costs a credit and can return slightly different text.
  • Use your own IDs as application_id and Rank ids, so results map back to your records.
  • Handle 422 from CV Extract as a permanent outcome for that file (not a resume, unreadable, empty).
  • Send do_not_store_data=true if you keep your own copy of the files: HireLayer then does not store the resume file.
  • Keep a human in the loop: scores and rankings support a recruiter, they do not replace one.