# HeadHunter API (hh.ru): Опубликовать вакансию от имени работодателя

Сценарий из mcp-everything. Актуально на 2026-10-24.

Размещать вакансии из своей ATS или карьерного сайта сразу на hh.ru.

1. Зарегистрируй приложение на dev.hh.ru. Отправь менеджера работодателя на https://hh.ru/oauth/authorize с response_type = code, client_id и redirect_uri.
2. Обменяй код на токены: POST /token с grant_type = authorization_code, client_id, client_secret, code и redirect_uri (тело — form-urlencoded). Обновить пару — тот же метод с refresh_token, но только после истечения access_token.
3. Узнай ID работодателя: GET /me — поле employer.id.
4. Опубликуй: POST /vacancies с name, description (HTML), area, professional_roles и при необходимости salary_range, experience, work_format, key_skills. Ответ 201. Услуги списываются с пакета работодателя; все вакансии проходят модерацию и появляются в поиске через несколько минут.
5. Изменить вакансию — PUT /vacancies/{vacancy_id} (составные поля вроде salary_range передавай целиком). Снять с публикации — PUT /employers/{employer_id}/vacancies/archived/{vacancy_id}.

```bash
curl -X POST https://api.hh.ru/vacancies \
  -H "Authorization: Bearer $HH_TOKEN" \
  -H 'HH-User-Agent: MyApp/1.0 (dev@example.ru)' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Python-разработчик",
        "description": "<p>Разрабатываем сервисы для логистики.</p><ul><li>Python 3, FastAPI</li></ul>",
        "area": {"id": "1"},
        "professional_roles": [{"id": "96"}],
        "salary_range": {"from": 200000, "to": 300000, "currency": "RUR", "gross": false, "mode": {"id": "MONTH"}, "frequency": {"id": "TWICE_PER_MONTH"}},
        "key_skills": [{"name": "Python"}, {"name": "PostgreSQL"}]
      }'
```

## Базовые URL

- Боевой: `https://api.hh.ru`
- Запросы ко всем сайтам группы идут на один адрес; нужный сайт задаётся параметром  (по умолчанию ), локаль: `?host=`

## Авторизация

Тип: OAuth 2.0

OAuth 2.0. Токен передаётся в заголовке `Authorization: Bearer {ACCESS_TOKEN}`.

- Токен пользователя (authorization code): пользователь открывает `https://hh.ru/oauth/authorize?response_type=code&client_id={client_id}&state={state}&redirect_uri={redirect_uri}`, затем `authorization_code` из редиректа обменивается на `access_token` и `refresh_token` запросом `POST https://api.hh.ru/token`. Поддерживается PKCE (`code_challenge`, `code_challenge_method=S256`).
- Обновление: `POST https://api.hh.ru/token` с `refresh_token`; refresh-токен одноразовый и работает только после истечения `access_token` (срок — `expires_in`, в секундах).
- Токен приложения (client credentials): срок жизни не ограничен, при повторном запросе прежний отзывается; запрашивать не чаще раза в 5 минут. Актуальный токен виден владельцу на `https://dev.hh.ru/admin`.

Параметры `/oauth/authorize`: `skip_choose_account=true` — не показывать выбор аккаунта, `force_login=true` — всегда показывать форму входа, `role` (`applicant` или `employer`) с `force_role=true` — требовать нужную роль.

В каждом запросе обязателен заголовок `User-Agent` или `HH-User-Agent` (при обоих учитывается `HH-User-Agent`); без него — `400 Bad Request`.

```
User-Agent: MyApp/1.0 (my-app-feedback@example.com)
```

## Методы сценария

### POST /token — Получение access-токена

Тело — `application/x-www-form-urlencoded`. Набор полей зависит от `grant_type` (получение токена пользователя, обновление пары токенов, токен приложения). Ниже — поля для обмена `authorization_code`.

В ответе: `access_token`, `token_type` (`bearer`), `expires_in`, `refresh_token`.

| Параметр | Где | Тип | Обяз. | Описание |
|---|---|---|---|---|
| `HH-User-Agent` | header | string | да | Название приложения и почта разработчика |
| `grant_type` | body | string | да | authorization_code |
| `client_id` | body | string | да | Идентификатор приложения |
| `client_secret` | body | string | да | Секретный ключ приложения |
| `code` | body | string | да | Полученный authorization_code |
| `redirect_uri` | body | string | да | URI перенаправления (если не указан — из настроек приложения) |
| `code_verifier` | body | string | нет | Верификатор PKCE; обязателен, если передавался code_challenge |

### GET /me — Информация о текущем пользователе

В ответе: `auth_type`, `id`, `email`, `first_name`, `last_name`, `is_applicant`, `is_employer`, `employer`, `manager`, `counters` и др.

| Параметр | Где | Тип | Обяз. | Описание |
|---|---|---|---|---|
| `HH-User-Agent` | header | string | да | Название приложения и почта разработчика |

### POST /vacancies — Публикация вакансии

При успешной публикации списываются услуги работодателя; конкретный пакет задаётся `employer_service_id`, иначе выбирается автоматически. Все вакансии проходят ручную модерацию и появляются в поиске через несколько минут. Успех — `201`.

| Параметр | Где | Тип | Обяз. | Описание |
|---|---|---|---|---|
| `ignore_duplicates` | query | boolean | нет | Принудительно добавить дубликат |
| `name` | body | string | да | Название |
| `description` | body | string | да | Описание в HTML |
| `area` | body | object | да | Регион |
| `professional_roles` | body | array | да | Профессиональные роли |
| `vacancy_properties` | body | object | нет | Свойства публикации (заменяет устаревшие type и billing_type) |
| `employer_service_id` | body | string | нет | Пакет услуг из suitable_packages[].employer_service_id |
| `salary_range` | body | object | нет | Зарплата (поле salary устарело) |
| `address` | body | object | нет | Адрес |
| `experience` | body | object | нет | Опыт работы |
| `employment_form` | body | object | нет | Тип занятости |
| `work_format` | body | array | нет | Форматы работы |
| `work_schedule_by_days` | body | array | нет | Графики работы |
| `working_hours` | body | array | нет | Рабочие часы в день |
| `key_skills` | body | array | нет | Ключевые навыки |
| `contacts` | body | object | нет | Контакты; при использовании нужен атрибут phones |
| `code` | body | string | нет | Внутренний код вакансии |
| `department` | body | object | нет | Департамент работодателя |
| `manager` | body | object | нет | Менеджер вакансии |
| `previous_id` | body | string | нет | ID архивной вакансии для связи с новой |
| `response_letter_required` | body | boolean | нет | Обязательно сопроводительное письмо |
| `test` | body | object | нет | Тест для вакансии |

### PUT /vacancies/{vacancy_id} — Редактирование вакансии

Изменяет переданные поля. Составные поля (`salary_range`, `contacts`, `professional_roles`) передаются целиком. `billing_type` и `manager` меняются отдельным запросом.

| Параметр | Где | Тип | Обяз. | Описание |
|---|---|---|---|---|
| `vacancy_id` | path | string | да | Идентификатор вакансии |
| `ignore_duplicates` | query | boolean | нет | Игнорировать появление дубликата (по умолчанию false) |
| `ignore_replacement_warning` | query | boolean | нет | Игнорировать ошибку о значительном изменении (по умолчанию true, есть риск блокировки) |
| `name` | body | string | нет | Название |
| `description` | body | string | нет | Описание в HTML |
| `salary_range` | body | object | нет | Зарплата (целиком) |
| `contacts` | body | object | нет | Контакты (целиком) |
| `professional_roles` | body | array | нет | Профессиональные роли (целиком) |

### PUT /employers/{employer_id}/vacancies/archived/{vacancy_id} — Архивация вакансии

Успех — `204`.

| Параметр | Где | Тип | Обяз. | Описание |
|---|---|---|---|---|
| `employer_id` | path | string | да | Идентификатор работодателя |
| `vacancy_id` | path | string | да | Идентификатор вакансии |

## Нюансы

- Пагинация: `page=N&per_page=M`, нумерация с нуля; ответ — `found`, `per_page`, `pages`, `page`, `items`.
- Неизвестные и недокументированные параметры игнорируются; недокументированные поля в ответах не рекомендуется использовать.
- Длинные ответы сжимаются gzip.
- Поддерживается CORS; для отладки есть `/cors` (HEAD, GET, POST, PUT, DELETE), для JSONP — `?callback=callback_name`.
- Вебхуки: на одно приложение — один URL. Ваш сервер отвечает `2xx` или `409` для дубликата (определяется по `id`). Без ответа за 5 секунд колбэк отправляется повторно; доставка не гарантирована. При долгих сбоях подписка блокируется, при отзыве доступа удаляется.
- Методы `/negotiations/{nid}/messages` устарели — переписка ведётся через `/common/chats`.
- Просмотр резюме с контактами списывает контакт один раз на компанию; если контакты скрыты соискателем, просмотр всё равно спишется — проверяйте `hidden_fields`.

## Другие сценарии

- [Найти вакансии по запросу](https://mcp-everything.ru/integrations/hh-api/scenarios/nayti-vakansii-po-zaprosu.md)
- [Разобрать отклики на вакансию](https://mcp-everything.ru/integrations/hh-api/scenarios/razobrat-otkliki-na-vakansiyu.md)
- [Переписываться с кандидатом в чате](https://mcp-everything.ru/integrations/hh-api/scenarios/perepisyvatsya-s-kandidatom-v-chate.md)
- [Получать уведомления о новых откликах](https://mcp-everything.ru/integrations/hh-api/scenarios/poluchat-uvedomleniya-o-novyh-otklikah.md)

---

Если этого недостаточно — официальная документация: https://api.hh.ru/openapi/redoc
Вся интеграция одним документом: https://mcp-everything.ru/integrations/hh-api.md
