# Jobok API

Данные рынка из сборов навыков hh.ru. Ключ — в разделе «API» кабинета

- Адрес API: https://api.jobok.ru
- Ключ — заголовок `Authorization: Bearer jbk_…`; создаётся в разделе «API» кабинета и показывается один раз
- Спека OpenAPI: https://api.jobok.ru/v1/openapi.json
- MCP: https://api.jobok.ru/v1/mcp — те же методы инструментами для ИИ-ассистентов

## Пределы

Не больше 60 запросов в минуту и 1000 в сутки — на все ключи владельца вместе, сутки по Москве. У ключа может быть свой суточный предел, меньше общего. Сверка с вакансией, которую ещё не читали, берёт её с hh.ru — таких не больше 50 в сутки; прочитанные отвечают из кэша без этого предела.

| Заголовок ответа | Что значит |
| --- | --- |
| `X-RateLimit-Limit-Requests` | суточный предел этого ключа |
| `X-RateLimit-Remaining-Requests` | сколько запросов осталось на сегодня |
| `X-RateLimit-Reset-Requests` | секунд до полуночи по Москве |
| `Retry-After` | на отказе 429 — через сколько секунд повторить |

## Ошибки

Отказ всегда в одной форме: `{ "error": { "type": "…", "message": "…" } }`. Ветвиться — по `type`; `message` — для человека и может меняться.

| type | Статус | Когда |
| --- | --- | --- |
| `invalid_api_key` | 401 | Ключа нет в заголовке, он не распознан или удалён |
| `key_expired` | 401 | Срок ключа истёк |
| `rate_limit_exceeded` | 429 | Исчерпан предел — повторить через Retry-After секунд |
| `not_found` | 404 | Сбора нет, у него ещё нет результата или такого метода нет вовсе |
| `invalid_request` | 400 | Неверные параметры или тело запроса — не JSON |
| `request_too_large` | 413 | Тело запроса больше 100 КБ |
| `hh_unavailable` | 502 | hh.ru не отдал страницу вакансии — повторить позже |
| `server_error` | 500 | Сбой на нашей стороне |

## Ключ

Пределы ключа и сколько из них потрачено сегодня

### Ключ и пределы

`GET /v1/key`

Пределы ключа и сколько потрачено за сегодняшние сутки по Москве. Этот запрос тоже идёт в счёт

**Ответ 200**

| Поле | Тип | Описание |
| --- | --- | --- |
| `name` | `string` |  |
| `hint` | `string` |  |
| `createdAt` | `string` |  |
| `expiresAt` | `string \| null` | null — бессрочный |
| `limits` | `object` |  |
| `limits.perMinute` | `number` | Запросов в минуту на все ключи владельца |
| `limits.perDay` | `number` | Запросов в сутки по Москве на все ключи владельца |
| `limits.keyPerDay` | `number \| null` | Свой суточный предел этого ключа; null — только общий |
| `limits.vacancyFetchesPerDay` | `number` | Вакансий в сутки, которые сверка может прочитать с hh.ru, на все ключи владельца; вакансии из кэша в счёт не идут |
| `usedToday` | `object` | Счёт сегодняшних суток по Москве, включая этот запрос |
| `usedToday.total` | `number` | Запросов за сегодня по всем ключам владельца |
| `usedToday.key` | `number` | Из них — этим ключом |
| `usedToday.vacancyFetches` | `number` | Вакансий прочитано с hh.ru по всем ключам владельца |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  "https://api.jobok.ru/v1/key"
```

```json
{
  "name": "Бот в Telegram",
  "hint": "jbk_AbCd…Wx12",
  "createdAt": "2026-10-11T19:03:06.623Z",
  "expiresAt": null,
  "limits": {
    "perMinute": 60,
    "perDay": 1000,
    "keyPerDay": null,
    "vacancyFetchesPerDay": 50
  },
  "usedToday": {
    "total": 94,
    "key": 1,
    "vacancyFetches": 7
  }
}
```

**Отказы**

| Код | Когда |
| --- | --- |
| 401 | Ключа нет, он удалён или истёк |
| 429 | Исчерпан предел — см. Retry-After |

## Сборы навыков

Готовые сборы навыков с hh.ru: что требуют работодатели по профессии и сколько платят

### Список сборов

`GET /v1/scans`

Готовые сборы навыков, свежие сверху, не больше 50

**Ответ 200** — массив объектов

| Поле | Тип | Описание |
| --- | --- | --- |
| `id` | `string` |  |
| `queries` | `string[]` | Формулировки названия вакансии, по которым шёл сбор |
| `status` | `"running" \| "done" \| "stopped" \| "failed"` | done — досчитан; stopped — остановлен вручную, посчитано то, что успели; running — идёт пересчёт, данные прошлого прогона |
| `scanned` | `number` | Сколько вакансий обойдено — база всех долей |
| `finishedAt` | `string \| null` |  |
| `skillCount` | `number` | Сколько групп навыков в сборе |
| `salary` | `object \| null` | null — зарплату не считали или её никто не указал |
| `salary.from` | `number \| null` | Медиана названных нижних границ, ₽ в месяц на руки |
| `salary.to` | `number \| null` | Медиана названных верхних границ, ₽ в месяц на руки |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  "https://api.jobok.ru/v1/scans"
```

```json
[
  {
    "id": "cmv1zfxir00041kx2cvn26e1g",
    "queries": [
      "devops инженер"
    ],
    "status": "done",
    "scanned": 407,
    "finishedAt": "2026-10-10T05:59:29.578Z",
    "skillCount": 112,
    "salary": {
      "from": 130500,
      "to": 226200
    }
  }
]
```

**Отказы**

| Код | Когда |
| --- | --- |
| 401 | Ключа нет, он удалён или истёк |
| 429 | Исчерпан предел — см. Retry-After |

### Сбор целиком

`GET /v1/scans/{id}`

Навыки с долями и зарплатой, квартили зарплат, слова названий, регионы

**Параметры**

| Параметр | Где | Тип | Описание |
| --- | --- | --- | --- |
| `id` | в пути | `string` | id сбора из GET /v1/scans |

**Ответ 200**

| Поле | Тип | Описание |
| --- | --- | --- |
| `id` | `string` |  |
| `queries` | `string[]` |  |
| `status` | `"running" \| "done" \| "stopped" \| "failed"` | done — досчитан; stopped — остановлен вручную, посчитано то, что успели; running — идёт пересчёт, данные прошлого прогона |
| `found` | `number` | Сколько вакансий нашёл hh.ru |
| `scanned` | `number` | Сколько обойдено — база всех долей |
| `limitHit` | `boolean` | hh.ru отдаёт не больше 2000 вакансий на запрос — обойдена не вся выдача |
| `finishedAt` | `string \| null` |  |
| `salary` | `object \| null` | null — зарплату не считали |
| `salary.from` | `object` | Нижние границы вилок («от»), ₽ в месяц на руки |
| `salary.from.p25` | `number \| null` |  |
| `salary.from.p50` | `number \| null` |  |
| `salary.from.p75` | `number \| null` |  |
| `salary.from.count` | `number` | По скольким вакансиям названа эта граница |
| `salary.to` | `object` | Верхние границы вилок («до»), ₽ в месяц на руки |
| `salary.to.p25` | `number \| null` |  |
| `salary.to.p50` | `number \| null` |  |
| `salary.to.p75` | `number \| null` |  |
| `salary.to.count` | `number` | По скольким вакансиям названа эта граница |
| `salary.withSalary` | `number` | У скольких вакансий зарплата пригодна к счёту |
| `salary.ratesAt` | `string \| null` | Дата курса ЦБ, по которому пересчитана валюта |
| `skills` | `object[]` | Все группы навыков, от частых к редким |
| `skills[].name` | `string` |  |
| `skills[].members` | `string[]` | Все написания навыка, от частого к редкому |
| `skills[].count` | `number` | Во скольких вакансиях встретился |
| `skills[].share` | `number` | Доля от обойдённых вакансий, 0..1 |
| `skills[].salaryFrom` | `number \| null` | Медиана «от» по вакансиям с этим навыком, ₽ в месяц |
| `skills[].salaryTo` | `number \| null` | Медиана «до» по вакансиям с этим навыком, ₽ в месяц |
| `titleWords` | `object[]` |  |
| `titleWords[].word` | `string` | Слово или устойчивое сочетание из названий вакансий |
| `titleWords[].count` | `number` |  |
| `titleWords[].employers` | `number` | У скольких разных работодателей встретилось |
| `areas` | `object[]` | Регионы вакансий |
| `areas[].name` | `string` |  |
| `areas[].count` | `number` |  |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  "https://api.jobok.ru/v1/scans/cmv1zfxir00041kx2cvn26e1g"
```

```json
{
  "id": "cmv1zfxir00041kx2cvn26e1g",
  "queries": [
    "devops инженер"
  ],
  "status": "done",
  "found": 448,
  "scanned": 407,
  "limitHit": false,
  "finishedAt": "2026-10-10T05:59:29.578Z",
  "salary": {
    "from": {
      "p25": 80000,
      "p50": 130500,
      "p75": 200000,
      "count": 68
    },
    "to": {
      "p25": 123467,
      "p50": 226200,
      "p75": 280000,
      "count": 58
    },
    "withSalary": 91,
    "ratesAt": "2026-10-10T00:00:00.000Z"
  },
  "skills": [
    {
      "name": "CI/CD",
      "members": [
        "CI/CD"
      ],
      "count": 47,
      "share": 0.1155,
      "salaryFrom": 115250,
      "salaryTo": 130250
    }
  ],
  "titleWords": [
    {
      "word": "devops-инженер",
      "count": 262,
      "employers": 204
    }
  ],
  "areas": [
    {
      "name": "Москва",
      "count": 213
    }
  ]
}
```

**Отказы**

| Код | Когда |
| --- | --- |
| 401 | Ключа нет, он удалён или истёк |
| 404 | Сбора нет или у него ещё нет результата |
| 429 | Исчерпан предел — см. Retry-After |

### Навыки профессии

`GET /v1/skills`

Самые частые навыки по профессии — из подходящего готового сбора или из названного по scanId

**Параметры**

| Параметр | Где | Тип | Описание |
| --- | --- | --- | --- |
| `profession` | в запросе, необязательный | `string` | Название профессии: сбор подбирается по словам названия — все слова одного должны быть в другом |
| `scanId` | в запросе, необязательный | `string` | Взять этот сбор (id из GET /v1/scans), а не подбирать по профессии |

**Ответ 200**

| Поле | Тип | Описание |
| --- | --- | --- |
| `scan` | `object` | Сбор, из которого взяты навыки |
| `scan.id` | `string` |  |
| `scan.queries` | `string[]` |  |
| `scan.scanned` | `number` | Сколько вакансий обойдено — база долей |
| `scan.finishedAt` | `string \| null` |  |
| `skills` | `object[]` | Самые частые навыки, не больше 40 |
| `skills[].name` | `string` |  |
| `skills[].members` | `string[]` | Все написания — по ним навык ищут в тексте резюме |
| `skills[].share` | `number` | Доля от обойдённых вакансий, 0..1 |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  "https://api.jobok.ru/v1/skills?profession=devops"
```

```json
{
  "scan": {
    "id": "cmv1zfxir00041kx2cvn26e1g",
    "queries": [
      "devops инженер"
    ],
    "scanned": 407,
    "finishedAt": "2026-10-10T05:59:29.578Z"
  },
  "skills": [
    {
      "name": "CI/CD",
      "members": [
        "CI/CD"
      ],
      "share": 0.1155
    }
  ]
}
```

**Отказы**

| Код | Когда |
| --- | --- |
| 400 | Нет ни profession, ни scanId |
| 401 | Ключа нет, он удалён или истёк |
| 404 | Подходящего сбора нет |
| 429 | Исчерпан предел — см. Retry-After |

## Сверка резюме

Какие навыки названы в тексте резюме, а каких не хватает — по рынку профессии или по одной вакансии

### Сверка с рынком

`POST /v1/resume/coverage`

Какие частые навыки профессии названы в тексте резюме, а каких не хватает

**Тело запроса, JSON**

| Поле | Тип | Обязательное | Описание |
| --- | --- | --- | --- |
| `profession` | `string` | нет | Название профессии: сбор подбирается по словам названия — все слова одного должны быть в другом |
| `scanId` | `string` | нет | Взять этот сбор (id из GET /v1/scans), а не подбирать по профессии |
| `text` | `string` | да | Текст резюме одной строкой: опыт, «О себе», навыки. Сервер его не хранит и не пишет в журнал |

**Ответ 200**

| Поле | Тип | Описание |
| --- | --- | --- |
| `scan` | `object` | Сбор, с навыками которого сверялся текст |
| `scan.id` | `string` |  |
| `scan.queries` | `string[]` |  |
| `scan.scanned` | `number` | Сколько вакансий обойдено — база долей |
| `scan.finishedAt` | `string \| null` |  |
| `covered` | `object[]` | Названы в тексте — любым написанием и отдельным словом: «Java» внутри «JavaScript» не считается |
| `covered[].name` | `string` |  |
| `covered[].members` | `string[]` | Все написания — по ним навык ищут в тексте резюме |
| `covered[].share` | `number` | Доля от обойдённых вакансий, 0..1 |
| `missing` | `object[]` | Не названы, частые сверху |
| `missing[].name` | `string` |  |
| `missing[].members` | `string[]` | Все написания — по ним навык ищут в тексте резюме |
| `missing[].share` | `number` | Доля от обойдённых вакансий, 0..1 |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"profession":"devops","text":"Настраивал CI/CD в GitLab, инфраструктура на Terraform и Ansible"}' \
  "https://api.jobok.ru/v1/resume/coverage"
```

```json
{
  "scan": {
    "id": "cmv1zfxir00041kx2cvn26e1g",
    "queries": [
      "devops инженер"
    ],
    "scanned": 407,
    "finishedAt": "2026-10-10T05:59:29.578Z"
  },
  "covered": [
    {
      "name": "CI/CD",
      "members": [
        "CI/CD"
      ],
      "share": 0.1155
    }
  ],
  "missing": [
    {
      "name": "Linux",
      "members": [
        "Linux"
      ],
      "share": 0.1081
    }
  ]
}
```

**Отказы**

| Код | Когда |
| --- | --- |
| 400 | Нет текста или нет ни profession, ни scanId |
| 401 | Ключа нет, он удалён или истёк |
| 404 | Подходящего сбора нет |
| 429 | Исчерпан предел — см. Retry-After |

### Сверка с вакансией

`POST /v1/vacancies/{id}/coverage`

Какие навыки вакансии hh.ru названы в тексте резюме, а каких не хватает. Вакансия, которую ещё не читали, берётся с hh.ru — у этого свой суточный предел (limits.vacancyFetchesPerDay в GET /v1/key); прочитанная отвечает из кэша без него

**Параметры**

| Параметр | Где | Тип | Описание |
| --- | --- | --- | --- |
| `id` | в пути | `string` | Номер вакансии hh.ru — цифры из ссылки hh.ru/vacancy/… |

**Тело запроса, JSON**

| Поле | Тип | Обязательное | Описание |
| --- | --- | --- | --- |
| `text` | `string` | да | Текст резюме одной строкой: опыт, «О себе», навыки. Сервер его не хранит и не пишет в журнал |

**Ответ 200**

| Поле | Тип | Описание |
| --- | --- | --- |
| `vacancy` | `object` |  |
| `vacancy.id` | `string` |  |
| `vacancy.url` | `string` |  |
| `covered` | `object[]` | Навыки вакансии, названные в тексте резюме |
| `covered[].name` | `string` |  |
| `covered[].members` | `string[]` | Написания навыка — по ним он ищется в тексте резюме: найденные в вакансии и известные по прошлым сборам |
| `covered[].source` | `"key_skills" \| "description"` | key_skills — в блоке «Ключевые навыки» вакансии; description — найден в описании по словарю прошлых сборов |
| `missing` | `object[]` | Не названы — сначала из блока «Ключевые навыки», потом из описания |
| `missing[].name` | `string` |  |
| `missing[].members` | `string[]` | Написания навыка — по ним он ищется в тексте резюме: найденные в вакансии и известные по прошлым сборам |
| `missing[].source` | `"key_skills" \| "description"` | key_skills — в блоке «Ключевые навыки» вакансии; description — найден в описании по словарю прошлых сборов |

**Пример**

```bash
curl -H "Authorization: Bearer $JOBOK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Настраивал CI/CD в GitLab, инфраструктура на Terraform и Ansible"}' \
  "https://api.jobok.ru/v1/vacancies/123456789/coverage"
```

```json
{
  "vacancy": {
    "id": "123456789",
    "url": "https://hh.ru/vacancy/123456789"
  },
  "covered": [
    {
      "name": "Vue.js",
      "members": [
        "Vue.js",
        "Vue",
        "VueJS"
      ],
      "source": "key_skills"
    }
  ],
  "missing": [
    {
      "name": "JavaScript",
      "members": [
        "JavaScript"
      ],
      "source": "key_skills"
    }
  ]
}
```

**Отказы**

| Код | Когда |
| --- | --- |
| 400 | Нет текста или id — не номер вакансии |
| 401 | Ключа нет, он удалён или истёк |
| 404 | Такой вакансии на hh.ru нет |
| 429 | Исчерпан предел — см. Retry-After |
| 502 | hh.ru не отдал страницу вакансии |

## MCP

Адрес `https://api.jobok.ru/v1/mcp`, Streamable HTTP без сессий, ключ — тот же заголовок `Authorization`. Каждое сообщение MCP идёт в счёт пределов.

| Инструмент | Что делает |
| --- | --- |
| `list_scans` | Готовые сборы навыков с hh.ru, свежие сверху: формулировки поиска (профессия), сколько вакансий обойдено, медианы зарплат. id сбора — для get_scan и scanId |
| `get_scan` | Сбор целиком: все навыки с долей вакансий и зарплатой, квартили зарплат, частые слова названий, регионы. Ответ большой — для частых навыков профессии хватит get_skills |
| `get_skills` | Самые частые навыки по профессии (до 40) с долей вакансий, где их просят. Сбор подбирается по названию профессии; если не подходит — scanId из list_scans |
| `check_resume_coverage` | Какие из частых навыков профессии названы в тексте резюме, а каких не хватает. Текст резюме не хранится |
| `check_vacancy_coverage` | Какие навыки конкретной вакансии hh.ru названы в тексте резюме, а каких не хватает. Вакансия — ссылка или номер; непрочитанная берётся с hh.ru, у этого свой суточный предел (get_limits) |
| `get_limits` | Пределы ключа и сколько потрачено сегодня |

| Промпт | Что делает |
| --- | --- |
| `resume_review` | Разбор резюме: каких навыков рынка или вакансии не хватает, что вписать и что убрать |
