# HireLayer Match

> Evaluate a resume against job criteria, criterion by criterion, with an explained status and a weighted 0–1 score.

Source: https://onlineresumeparser.com/api-docs/match · Markdown: https://onlineresumeparser.com/api-docs/match.md

- **Endpoint:** `POST /api/v1/matching/job-candidate`
- **Input:** JSON · job text, resume text and criteria
- **Output:** Score from 0 to 1, a status and an explanation per criterion
- **Billing:** 1 credit per successful call

## Match a candidate to a job

`POST https://onlineresumeparser.com/api/v1/matching/job-candidate`

Evaluate one resume against each job criterion and get an explained 0–1 score.

- **Authentication:** `X-API-Key` header
- **Content type:** `application/json`
- **Billing:** 1 credit per successful call
- **Client timeout:** at least 65 seconds

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `job_text` | `string` | Yes | Job description. Trimmed before validation. 1–50,000 characters. |
| `candidate_text` | `string` | Yes | Candidate resume as plain text, e.g. `info_resume.text` from HireLayer CV Extract. Trimmed before validation. 1–50,000 characters. |
| `matching_criteria` | `MatchingCriterion[]` | Yes | Criteria to evaluate, usually from HireLayer Job Extract. Can be empty. No other criterion field is accepted. |
| `matching_criteria[].id` | `string` | Yes | Criterion ID. Job Extract generates `crit_1`…`crit_n`; Match accepts any non-empty string. Non-empty. |
| `matching_criteria[].label` | `string` | Yes | What is evaluated, in a short phrase. Non-empty. |
| `matching_criteria[].weight` | `integer` | Yes | Importance: `3` essential, `2` important, `1` nice to have. 1–3. |
| `matching_criteria[].is_mandatory` | `boolean` | Yes | Whether the job states it as a hard requirement. Informational: it does not change the Match score. |
| `matching_criteria[].rationale` | `string` | Yes | Why the criterion matters for the job. Non-empty. |

### Example request

**cURL**

```bash
curl -X POST https://onlineresumeparser.com/api/v1/matching/job-candidate \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.",
    "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.",
    "matching_criteria": [
      {
        "id": "crit_1",
        "label": "Maîtrise de React",
        "weight": 3,
        "is_mandatory": true,
        "rationale": "React est explicitement exigé pour le poste."
      },
      {
        "id": "crit_2",
        "label": "Maîtrise de TypeScript",
        "weight": 3,
        "is_mandatory": true,
        "rationale": "TypeScript est explicitement exigé pour le poste."
      },
      {
        "id": "crit_3",
        "label": "Au moins 5 ans d’expérience en développement frontend",
        "weight": 3,
        "is_mandatory": true,
        "rationale": "L’offre demande plus de cinq ans d’expérience frontend."
      },
      {
        "id": "crit_4",
        "label": "Anglais courant",
        "weight": 2,
        "is_mandatory": true,
        "rationale": "Un anglais courant est demandé."
      },
      {
        "id": "crit_5",
        "label": "Expérience avec Next.js",
        "weight": 1,
        "is_mandatory": false,
        "rationale": "Next.js est présenté comme un atout."
      }
    ]
  }'
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://onlineresumeparser.com/api/v1/matching/job-candidate",
    headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
    json={
        "job_text": "Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.",
        "candidate_text": "Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.",
        "matching_criteria": [
            {
                "id": "crit_1",
                "label": "Maîtrise de React",
                "weight": 3,
                "is_mandatory": True,
                "rationale": "React est explicitement exigé pour le poste.",
            },
            {
                "id": "crit_2",
                "label": "Maîtrise de TypeScript",
                "weight": 3,
                "is_mandatory": True,
                "rationale": "TypeScript est explicitement exigé pour le poste.",
            },
            {
                "id": "crit_3",
                "label": "Au moins 5 ans d’expérience en développement frontend",
                "weight": 3,
                "is_mandatory": True,
                "rationale": "L’offre demande plus de cinq ans d’expérience frontend.",
            },
            {
                "id": "crit_4",
                "label": "Anglais courant",
                "weight": 2,
                "is_mandatory": True,
                "rationale": "Un anglais courant est demandé.",
            },
            {
                "id": "crit_5",
                "label": "Expérience avec Next.js",
                "weight": 1,
                "is_mandatory": False,
                "rationale": "Next.js est présenté comme un atout.",
            },
        ],
    },
    timeout=65,
)
response.raise_for_status()

data = response.json()
print(data["score"])
```

**TypeScript**

```typescript
const response = await fetch('https://onlineresumeparser.com/api/v1/matching/job-candidate', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    job_text: 'Senior Frontend Engineer, Paris (hybrid). You will build our recruiting platform with React and TypeScript. Requirements: 5+ years of frontend development, strong React and TypeScript skills, fluent English. Nice to have: experience with Next.js.',
    candidate_text: 'Alex Morgan, Senior Software Engineer in Paris. 8 years of experience. Since 2022, leads a team building a React and TypeScript SaaS platform at Northstar Labs. Previously built Node.js APIs. Professional English.',
    matching_criteria: [
      {
        id: 'crit_1',
        label: 'Maîtrise de React',
        weight: 3,
        is_mandatory: true,
        rationale: 'React est explicitement exigé pour le poste.',
      },
      {
        id: 'crit_2',
        label: 'Maîtrise de TypeScript',
        weight: 3,
        is_mandatory: true,
        rationale: 'TypeScript est explicitement exigé pour le poste.',
      },
      {
        id: 'crit_3',
        label: 'Au moins 5 ans d’expérience en développement frontend',
        weight: 3,
        is_mandatory: true,
        rationale: 'L’offre demande plus de cinq ans d’expérience frontend.',
      },
      {
        id: 'crit_4',
        label: 'Anglais courant',
        weight: 2,
        is_mandatory: true,
        rationale: 'Un anglais courant est demandé.',
      },
      {
        id: 'crit_5',
        label: 'Expérience avec Next.js',
        weight: 1,
        is_mandatory: false,
        rationale: 'Next.js est présenté comme un atout.',
      },
    ],
  }),
  signal: AbortSignal.timeout(65_000),
})

if (!response.ok) {
  throw new Error(`HireLayer ${response.status}: ${await response.text()}`)
}

const data = await response.json()
console.log(data.score)
```

### Response `200`

Evaluation of every criterion.

| Field | Type | Description |
| --- | --- | --- |
| `score` | `number` | Weighted average of the criterion statuses. Not rounded. 0–1. |
| `summary` | `string` | Overall assessment, in French. |
| `evaluated_criteria` | `EvaluatedCriterion[]` | Every input criterion, in input order, with its evaluation. |
| `evaluated_criteria[].id` | `string` | Criterion ID. Job Extract generates `crit_1`…`crit_n`; Match accepts any non-empty string. Non-empty. |
| `evaluated_criteria[].label` | `string` | What is evaluated, in a short phrase. Non-empty. |
| `evaluated_criteria[].weight` | `integer` | Importance: `3` essential, `2` important, `1` nice to have. 1–3. |
| `evaluated_criteria[].is_mandatory` | `boolean` | Whether the job states it as a hard requirement. Informational: it does not change the Match score. |
| `evaluated_criteria[].rationale` | `string` | Why the criterion matters for the job. Non-empty. |
| `evaluated_criteria[].match_status` | `string` | `ideal`: clearly met · `potential`: partly or indirectly met · `not_mentioned`: the resume says nothing · `not_valid`: contradicted. One of: `"ideal"`, `"potential"`, `"not_valid"`, `"not_mentioned"`. |
| `evaluated_criteria[].match_explanation` | `string` | Evidence from the resume, in French. |

```json
{
  "score": 0.8916666666666666,
  "summary": "Profil très aligné : React, TypeScript et l’expérience demandée sont démontrés. Le niveau d’anglais reste à confirmer.",
  "evaluated_criteria": [
    {
      "id": "crit_1",
      "label": "Maîtrise de React",
      "weight": 3,
      "is_mandatory": true,
      "rationale": "React est explicitement exigé pour le poste.",
      "match_status": "ideal",
      "match_explanation": "Le CV décrit une équipe React dirigée depuis 2022 sur une plateforme en production."
    },
    {
      "id": "crit_2",
      "label": "Maîtrise de TypeScript",
      "weight": 3,
      "is_mandatory": true,
      "rationale": "TypeScript est explicitement exigé pour le poste.",
      "match_status": "ideal",
      "match_explanation": "La plateforme actuelle est développée en TypeScript."
    },
    {
      "id": "crit_3",
      "label": "Au moins 5 ans d’expérience en développement frontend",
      "weight": 3,
      "is_mandatory": true,
      "rationale": "L’offre demande plus de cinq ans d’expérience frontend.",
      "match_status": "ideal",
      "match_explanation": "Le candidat cumule huit ans d’expérience en développement."
    },
    {
      "id": "crit_4",
      "label": "Anglais courant",
      "weight": 2,
      "is_mandatory": true,
      "rationale": "Un anglais courant est demandé.",
      "match_status": "potential",
      "match_explanation": "Le CV mentionne un anglais professionnel, sans préciser un niveau courant."
    },
    {
      "id": "crit_5",
      "label": "Expérience avec Next.js",
      "weight": 1,
      "is_mandatory": false,
      "rationale": "Next.js est présenté comme un atout.",
      "match_status": "not_mentioned",
      "match_explanation": "Le CV ne mentionne pas Next.js."
    }
  ]
}
```

### Errors

| Status | `error` | `code` | When | Retry |
| --- | --- | --- | --- | --- |
| `400` | `Request body must be a JSON object` | — | The body is missing, is a JSON array, or `Content-Type` is not `application/json`. | Do not retry |
| `400` | `The request contains unsupported fields` | — | The body contains a field that is not documented for this endpoint. | Do not retry |
| `400` | `The 'job_text' field is required` | — | `job_text` is missing or not a string. | Do not retry |
| `400` | `job_text cannot be empty` | — | `job_text` is empty after trimming. | Do not retry |
| `400` | `job_text must be 50000 characters or less` | — | `job_text` is longer than 50,000 characters after trimming. | Do not retry |
| `400` | `The 'candidate_text' field is required` | — | `candidate_text` is missing or not a string. | Do not retry |
| `400` | `candidate_text cannot be empty` | — | `candidate_text` is empty after trimming. | Do not retry |
| `400` | `candidate_text must be 50000 characters or less` | — | `candidate_text` is longer than 50,000 characters after trimming. | Do not retry |
| `400` | `The 'matching_criteria' field must be an array` | — | `matching_criteria` is missing or not an array. | Do not retry |
| `400` | `matching_criteria contains an invalid criterion` | — | A criterion has a missing, empty or extra field, or a `weight` outside 1–3. | Do not retry |
| `401` | `Missing API Key` | — | The `X-API-Key` header is absent. `Authorization: Bearer` is not accepted. | Do not retry |
| `401` | `Invalid API Key` | — | The key is malformed, unknown or revoked. Rotating a key revokes the previous one immediately. | Do not retry |
| `403` | `Insufficient credits available` | — | The account has no credit left. `availableCredits` is the current balance. | Fix, then retry |
| `500` | `Internal server error` | — | Unexpected gateway failure. | Retry with backoff |
| `500` | `Internal server error. Please try again later.` | — | The body is not valid JSON, is a JSON primitive or exceeds 10 MB, or the service failed unexpectedly. Check the payload: if it is valid, retry. | Retry with backoff |
| `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | Retry with backoff |
| `502` | `The AI processing step failed. Please try again later.` | — | Matching failed or returned an invalid result after the service's internal retries. | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Credits could not be checked or deducted. No credit is consumed, including when the operation itself had completed. | Retry with backoff |

Example error (`400`):

```json
{
  "success": false,
  "error": "Request body must be a JSON object"
}
```

### Behavior

- **Latency:** Synchronous. The gateway waits up to 60 seconds, then returns `502 Upstream API unavailable`. Use a client timeout of at least 65 seconds.
- **Retries:** Transient model errors are retried by the service before it answers. Retry `502` and `503` with exponential backoff; never retry `4xx` unchanged. Failed requests are not charged.
- **Validation:** Validation stops at the first error, so one call reports one problem. Text fields are trimmed before their length is checked.
- **Score:** `score = Σ(weight × value) / Σ weight` with `ideal` = 1, `potential` = 0.6, `not_mentioned` = 0.5 and `not_valid` = 0. Every criterion counts in the denominator; `is_mandatory` has no effect on the score.
- **Empty criteria:** With `"matching_criteria": []` the model is not called and the response is exactly `{"score": 0, "summary": "Aucun critère à évaluer.", "evaluated_criteria": []}`.
- **Hard requirements:** To reject candidates who miss a mandatory criterion, check `is_mandatory` and `match_status` in your code: the score alone does not do it.

> **Tip: Comparing several candidates?.** Use [HireLayer Rank](https://onlineresumeparser.com/api-docs/rank.md) to order up to 10 candidates for one job in a single call. Match gives the detailed, per-criterion explanation for one candidate.

## Next

- [HireLayer Rank](https://onlineresumeparser.com/api-docs/rank.md): Rank up to 10 candidates against one job description in a single call, with a score and a rationale for each.
- [Build a screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result.
