# HireLayer Skills

> Normalize free-text skills against the HireLayer catalog, one at a time or in batches of 100, and download the catalog.

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

- **Endpoints:** `POST /api/v1/skills/match` · `GET /api/v1/skills`
- **Input:** JSON · one skill, or up to 100
- **Output:** Closest catalog skills with a similarity score
- **Billing:** 1 credit per successful call, single or batch

## Match skills to the catalog

`POST https://onlineresumeparser.com/api/v1/skills/match`

Map free-text skills, one or up to 100 at a time, to the closest catalog skills.

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

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `skill` | `string` | No | One free-text skill. Send `skill` or `skills`, not both. Non-empty. |
| `skills` | `string[]` | No | Batch of free-text skills. Non-string items are converted with `String()`. 1–100 items. |
| `top_k` | `integer` | No | Matches per skill. Defaults to `5` with `skill`, `3` with `skills`. 1–50. |

Send exactly one of `skill` or `skills`.

### Example request

**cURL**

```bash
curl -X POST https://onlineresumeparser.com/api/v1/skills/match \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "skill": "react js",
    "top_k": 3
  }'
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://onlineresumeparser.com/api/v1/skills/match",
    headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
    json={
        "skill": "react js",
        "top_k": 3,
    },
    timeout=65,
)
response.raise_for_status()

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

**TypeScript**

```typescript
const response = await fetch('https://onlineresumeparser.com/api/v1/skills/match', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    skill: 'react js',
    top_k: 3,
  }),
  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.results)
```

### Response `200`

Single mode (`skill`).

| Field | Type | Description |
| --- | --- | --- |
| `query_skill` | `string` | Your skill, trimmed. |
| `total_results` | `integer` | Number of `results`, at most `top_k`. |
| `results` | `SkillMatch[]` | Closest catalog skills, best first. |
| `results[].rank` | `integer` | Position, 1 is the closest. ≥ 1. |
| `results[].skill` | `string` | Catalog label. |
| `results[].similarity_score` | `number` | Cosine similarity rounded to 4 decimals. `1` for an exact label or synonym match. ≤ 1. |
| `results[].domain` | `string` | Catalog domain, e.g. `Technologie`. |
| `results[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. |

```json
{
  "query_skill": "react js",
  "total_results": 3,
  "results": [
    {
      "rank": 1,
      "skill": "React",
      "similarity_score": 0.8712,
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks"
    },
    {
      "rank": 2,
      "skill": "React Native",
      "similarity_score": 0.7934,
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks"
    },
    {
      "rank": 3,
      "skill": "Javascript",
      "similarity_score": 0.7121,
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks"
    }
  ]
}
```

### Response `200`

Batch mode (`skills`).

| Field | Type | Description |
| --- | --- | --- |
| `total_queries` | `integer` | Number of skills sent. |
| `successful_matches` | `integer` | Number of items with `success: true`. |
| `results` | `SkillBatchItem[]` | One item per input skill, in input order. Duplicates are kept. |
| `results[].query_skill` | `string` | The input skill. |
| `results[].success` | `boolean` |  |
| `results[].results` | `SkillMatch[]` | May be absent. Present when `success` is `true`. |
| `results[].results[].rank` | `integer` | Position, 1 is the closest. ≥ 1. |
| `results[].results[].skill` | `string` | Catalog label. |
| `results[].results[].similarity_score` | `number` | Cosine similarity rounded to 4 decimals. `1` for an exact label or synonym match. ≤ 1. |
| `results[].results[].domain` | `string` | Catalog domain, e.g. `Technologie`. |
| `results[].results[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. |
| `results[].error` | `string` | May be absent. Present when `success` is `false`: `Empty skill`, or `The AI processing step failed. Please try again later.` when matching failed (retry this skill). |

### Batch request

Send `skills` to match up to 100 values in one call. Each item succeeds or fails on its own; the HTTP status stays `200`.

**cURL**

```bash
curl -X POST https://onlineresumeparser.com/api/v1/skills/match \
  -H "X-API-Key: $HIRELAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "skills": [
      "python",
      "gestion de projets",
      " "
    ],
    "top_k": 1
  }'
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://onlineresumeparser.com/api/v1/skills/match",
    headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
    json={
        "skills": [
            "python",
            "gestion de projets",
            " ",
        ],
        "top_k": 1,
    },
    timeout=65,
)
response.raise_for_status()

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

**TypeScript**

```typescript
const response = await fetch('https://onlineresumeparser.com/api/v1/skills/match', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    skills: [
      'python',
      'gestion de projets',
      ' ',
    ],
    top_k: 1,
  }),
  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.results)
```

```json
{
  "total_queries": 3,
  "successful_matches": 2,
  "results": [
    {
      "query_skill": "python",
      "success": true,
      "results": [
        {
          "rank": 1,
          "skill": "Python",
          "similarity_score": 1,
          "domain": "Technologie",
          "subcategory": "Languages & Frameworks"
        }
      ]
    },
    {
      "query_skill": "gestion de projets",
      "success": true,
      "results": [
        {
          "rank": 1,
          "skill": "Gestion de projet - PMO",
          "similarity_score": 1,
          "domain": "Business",
          "subcategory": ""
        }
      ]
    },
    {
      "query_skill": " ",
      "success": false,
      "error": "Empty skill"
    }
  ]
}
```

### Errors

| Status | `error` | `code` | When | Retry |
| --- | --- | --- | --- | --- |
| `400` | `Provide either 'skill' or 'skills', not both` | — | Both keys are present, even if one is `null`. | Do not retry |
| `400` | `The 'skill' or 'skills' field is required` | — | Neither key is present. | Do not retry |
| `400` | `The 'skill' field is required` | — | `skill` is not a string. | Do not retry |
| `400` | `Skill cannot be empty` | — | `skill` is empty after trimming. | Do not retry |
| `400` | `The 'skills' (list) field is required` | — | `skills` is not an array. | Do not retry |
| `400` | `The 'skills' field must be a non-empty list` | — | `skills` is `[]`. | Do not retry |
| `400` | `Maximum 100 skills per batch request` | — | `skills` has more than 100 items. | Do not retry |
| `400` | `top_k must be an integer between 1 and 50` | — | `top_k` is not a JSON integer from 1 to 50 (`"5"` and `null` are rejected). | Do not 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 |
| `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 |
| `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": "Provide either 'skill' or 'skills', not both"
}
```

### Behavior

- **Latency:** Synchronous. The gateway waits up to 60 seconds; use a client timeout of at least 65 seconds.
- **Exact matches:** When the normalized text (case, accents and punctuation ignored) equals a catalog label or synonym, that skill is ranked first with `similarity_score: 1`. Other results come from semantic similarity.
- **Thresholds:** Results are always returned, even weak ones. Choose your own `similarity_score` threshold; HireLayer CV Extract uses 0.75 to mark a skill as `normalized`.
- **Unknown fields:** Unlike the other endpoints, unknown body fields are ignored.

## List the skill catalog

`GET https://onlineresumeparser.com/api/v1/skills`

Download the whole reference catalog used for matching.

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

### Example request

**cURL**

```bash
curl https://onlineresumeparser.com/api/v1/skills \
  -H "X-API-Key: $HIRELAYER_API_KEY"
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://onlineresumeparser.com/api/v1/skills",
    headers={"X-API-Key": os.environ["HIRELAYER_API_KEY"]},
    timeout=65,
)
response.raise_for_status()

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

**TypeScript**

```typescript
const response = await fetch('https://onlineresumeparser.com/api/v1/skills', {
  headers: {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
  },
  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.total_skills)
```

### Response `200`

The catalog.

| Field | Type | Description |
| --- | --- | --- |
| `total_skills` | `integer` | Number of skills in the catalog. |
| `skills` | `CatalogSkill[]` | The whole catalog. There is no pagination. |
| `skills[].skill` | `string` | Catalog label. |
| `skills[].domain` | `string` | Catalog domain, e.g. `Technologie`. |
| `skills[].subcategory` | `string` | Catalog subcategory. Empty string when the skill has none. |
| `skills[].rank` | `integer` | 1-based position in the catalog. Not a relevance score. ≥ 1. |

```json
{
  "total_skills": 664,
  "skills": [
    {
      "skill": "GMAO",
      "domain": "Business",
      "subcategory": "",
      "rank": 1
    },
    {
      "skill": "Gantt",
      "domain": "Business",
      "subcategory": "",
      "rank": 2
    },
    {
      "skill": "Figma",
      "domain": "Design & Contenu",
      "subcategory": "Logiciel",
      "rank": 180
    },
    {
      "skill": "React",
      "domain": "Technologie",
      "subcategory": "Languages & Frameworks",
      "rank": 499
    }
  ]
}
```

### Errors

| Status | `error` | `code` | When | 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 |
| `502` | `Upstream API unavailable` | — | The service did not answer within 60 seconds, or could not be reached. | 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 (`401`):

```json
{
  "error": "Missing API Key"
}
```

### Behavior

- **Caching:** The catalog changes rarely and each call costs a credit: cache it on your side, for example once a day.
- **Labels:** Labels and domains are mostly French (`Technologie`, `Ressources Humaines`, `Design & Contenu`…). Query parameters are ignored.

## Next

- [HireLayer CV Extract](https://onlineresumeparser.com/api-docs/extract.md): Parse a resume file into structured candidate JSON with one multipart request: contact details, experience, education, languages and typed skills.
- [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.
