HH.ru Jobs Scraper - Salaries, Skills & 50+ Fields
Pricing
from $1.00 / 1,000 results
HH.ru Jobs Scraper - Salaries, Skills & 50+ Fields
From $1/1K. Scrape HH.ru job listings with 50+ structured fields. Search by filters or URLs and extract salary, experience, schedule, employment type, and role. Detail enrichment adds full descriptions, key skills, contact information, and employer logos.
Pricing
from $1.00 / 1,000 results
Rating
3.3
(3)
Developer
Abot API
Maintained by CommunityActor stats
4
Bookmarked
128
Total users
16
Monthly active users
8 days ago
Last modified
Categories
Share
hh.ru Jobs Scraper: Vacancies, Salaries, Skills & Employers
hh.ru Jobs Scraper turns hh.ru (HeadHunter), Russia's largest job board, into structured job data. Search by keyword, area, experience, schedule, salary and professional role, paste a search URL you already refined in a browser, or hand in a list of specific vacancy links or IDs. Get 50+ fields per vacancy, including full descriptions, key skills, salary ranges, metro stations and employer details, then export to JSON, CSV or Excel, or read it straight through the API.
Why This Scraper?
- Three ways to find data. Build a search from keywords, area and filters, paste hh.ru search URLs as-is, or hand in specific vacancy links or IDs and get one full detail card per entry.
- 50+ fields per vacancy. Salary ranges, GPS coordinates, metro stations, employer logos and badges, full HTML and plain-text descriptions, and key skills, plus employer details.
- Detail on demand. Turn full detail-page enrichment on or off; results still stream into the dataset as each one finishes, so a mid-run stop keeps everything collected so far.
- Full filter coverage. Search field (title-only or everywhere), experience, employment, schedule, salary range, only-with-salary, professional role, industry and sort order.
- Built for schedules. Incremental mode returns only new and changed vacancies on recurring runs, and a run that can't reach hh.ru fails loudly instead of returning an empty dataset.
- Clear about hh.ru's own 2000-result cap. Slice one broad search into narrower ones by area, salary band or role to capture everything beyond it.
Use Cases
- Recruiting and talent sourcing: build a candidate-facing job feed, or watch a market for hiring signals by company, role or region.
- Salary and compensation research: collect published salary ranges by role, region, and experience level to benchmark pay.
- Job aggregators and alert bots: feed vacancies into your own site, Telegram bot or newsletter, filtered by keyword, area or schedule.
- Market and competitor monitoring: track which companies are hiring, for which roles, and how listings change over time with incremental mode.
- Targeted outreach: hand in a specific list of vacancy URLs or IDs to get full detail cards, for example to reach out to companies that just posted a role.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
vacancyId / url | "100000001" / "https://hh.ru/vacancy/100000001" |
name | "Senior Python-разработчик" |
publicationDate / creationDate / lastChangeTime / validThroughTime | "2026-05-05T09:00:00.000+03:00" and similar (validThroughTime only when fetchDetails is on) |
salaryFrom / salaryTo / salaryCurrency / salaryGross / salaryMode / salaryFrequency | 200000 / 300000 / "RUR" / false / "MONTH" / "TWICE_PER_MONTH" |
area | { "id": 1, "name": "Москва", "path": ".113.1." } |
address | { "city": "Москва", "street": "Тверская улица", "lat": 55.7558, "lng": 37.6173, "metroStations": [...], "district": "Тверской район" } |
company | { "id": 1000000, "name": "ООО «Пример»", "isAccreditedIT": true, "isTrusted": true, "siteUrl": "...", "logoUrl": "...", "logos": [...], "badges": [...] } |
workExperience / employment / employmentForm | "between3And6" / "FULL" / "FULL" |
workSchedule / workScheduleByDays / workFormats / workingHours | "remote" / ["FIVE_ON_TWO_OFF"] / ["REMOTE"] / ["HOURS_8"] |
nightShifts / internship / acceptHandicapped / acceptLaborContract | false / false / false / true |
description / descriptionText | full HTML and plain-text description (when fetchDetails is on) |
keySkills | ["Python", "Django", "PostgreSQL", "Docker", "Git"] (when fetchDetails is on) |
contactInfo | { "name": null, "email": null, "phones": [], "contactsHidden": false } (when fetchDetails is on; hh.ru hides phone, email and name from logged-out visitors, so these are almost always empty) |
detailScraped | true, whether the detail page (description, skills, contact info) actually arrived on this row |
professionalRoleIds | [96, 156] |
responsesCount / totalResponsesCount / onlineUsersCount | 42 / 100 / 5 |
isAdv | false, whether the listing is a paid/promoted placement |
changeType / changedFields / firstSeenAt / lastSeenAt | incremental mode only: "UPDATED" / ["salaryFrom"] / timestamps |
searchUrl / searchSessionId | the search that produced this row |
scrapedAt | "2026-05-05T12:00:00.000Z" |
How to Use
- Pick a search mode:
search(keywords, area and filters below),url(paste hh.ru search URLs already refined in a browser) orvacancy(paste specific vacancy URLs or IDs for a full detail card each, no search). - In search mode, add keywords and narrow with area, experience, employment, schedule, salary and other filters. In URL mode, paste search URLs; the filter fields above are ignored.
- Turn
fetchDetailson or off, and set Max total listings and Max pages to control run size and cost. - Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.
Search by keyword and area:
{"mode": "search","queries": ["python"],"areas": ["1"],"maxPages": 5,"maxListings": 100,"fetchDetails": true,"proxy": { "useApifyProxy": true }}
Search with experience, remote and salary filters:
{"mode": "search","queries": ["data scientist"],"areas": ["1", "2"],"experience": "between3And6","schedule": ["remote"],"salaryMin": 200000,"onlyWithSalary": true,"orderBy": "salary_desc","proxy": { "useApifyProxy": true }}
Paste a search URL:
{"mode": "url","urls": ["https://hh.ru/search/vacancy?text=AI%20engineer&area=1&experience=between1And3&schedule=remote&page=0"],"maxPages": 5,"proxy": { "useApifyProxy": true }}
Specific vacancies by URL or ID:
{"mode": "vacancy","vacancyInput": ["https://hh.ru/vacancy/123456789", "123456790"],"proxy": { "useApifyProxy": true }}
Vacancy mode always returns the full card (description, key skills, contact info and logos; fetchDetails is implied) for each entry, with no search performed, so hh.ru's 2000-result cap never applies. A removed or archived vacancy is omitted from the dataset and never charged; the run log names each skipped one.
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/hh-ru-jobs-scraper").call(run_input={"mode": "search","queries": ["python"],"areas": ["1"],"proxy": {"useApifyProxy": True},})for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["name"], job["salaryFrom"], job["company"]["name"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/hh-ru-jobs-scraper').call({mode: 'search',queries: ['python'],areas: ['1'],proxy: { useApifyProxy: true },});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice. - Incremental mode (
incrementalMode) is for scheduled runs over the same search, URL list or vacancy list. Each row is classifiedNEW,UPDATED(withchangedFields),UNCHANGED(suppressed and not billed unlessemitUnchangedis on),REAPPEARED, orEXPIRED(only after a run that scanned the whole tracked search, with no cap hit or resume, and only withemitExpired).stateKeynames or shares the stored state. With incremental mode off, rows carry no change-tracking fields.
The 2000-result limit
hh.ru itself serves at most 2000 results per search filter (40 pages of 50), a server-side cap this actor cannot exceed. The run logs a warning when a search matches more than what's reachable. Split one broad search into narrower slices to capture the rest: by area (multiple entries instead of ["113"]), by salary band, by professional role or industry, or by recency with orderBy: "publication_time" run on a schedule. Each slice gets its own 2000-result budget, and results are deduplicated by vacancyId within a run.
Tips
- Relevant title matches by default.
searchField: "name"(the default) matches your keyword against the vacancy title, so a search stays on-topic. Set it to"everywhere"for hh.ru's own broader default, which also matches the description and company name. - Skip detail pages for a lighter run.
fetchDetails: falsereturns pure search-result fields (salary, area, address, company basics, work format) and leavesdescription,descriptionText,keySkills,contactInfoandvalidThroughTimeempty (null or[]). - Detail coverage at scale. Some detail pages are refused on the first try; the actor retries them automatically,
detailScrapedmarks whether each row's detail page arrived, and the run log reports the overall percentage.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search, url, or vacancy. |
queries | array | (none; prefilled AI) | Keywords, one search per keyword. Search mode only. |
searchField | string | name | Where the keyword is matched: name (title only), everywhere, company_name, or description. |
areas | array | ["113"] (whole country) | Area IDs (1=Moscow, 2=St Petersburg, 113=Russia). Search mode only. |
experience | string | any | any, noExperience, between1And3, between3And6, or moreThan6. |
employment | array | (none; all types) | Multi-select: full, part, project, volunteer, probation. |
schedule | array | (none; all schedules) | Multi-select: fullDay, shift, flexible, remote, flyInFlyOut. |
salaryMin | integer | (none) | Minimum monthly salary in RUB. |
onlyWithSalary | boolean | false | Only return listings that publish a salary. |
orderBy | string | relevance | relevance, publication_time, salary_desc, salary_asc, or distance. |
professionalRole | array | (none) | Numeric professional role IDs. |
industry | array | (none) | Numeric industry IDs. |
urls | array | (none; prefilled sample) | Search URLs. URL mode only; filters above are ignored. |
vacancyInput | array | (none; prefilled sample) | Vacancy URLs or bare numeric IDs. Vacancy mode only. |
maxPages | integer | 2 | Result pages to walk per query or URL, 1 to 40. Search and URL modes only. |
maxListings | integer | 0 | Hard cap across the run (0 = unlimited). |
fetchDetails | boolean | true | Adds description, key skills, contact info and full company logos. |
resumeFromRunId | string | (none) | Continue one interrupted run. |
incrementalMode | boolean | false | Return only new and changed vacancies on recurring runs. |
stateKey | string | (none; derived automatically) | Name or share an incremental-mode monitoring campaign. |
emitUnchanged | boolean | false | Also return (and bill) unchanged vacancies. |
emitExpired | boolean | false | Also return (and bill) expired vacancies. |
proxy | object | (none; prefilled Apify Proxy, datacenter) | Connection settings. Include it when calling from the API. |
mcpConnectors | array | (none) | Optional: send a summary of each vacancy to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which items are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. |
Output Example
Sample shape: values are illustrative placeholders, not from a live listing.
{"vacancyId": "100000001","url": "https://hh.ru/vacancy/100000001","name": "Senior Python-разработчик","publicationDate": "2026-05-05T09:00:00.000+03:00","creationDate": "2026-05-01T09:00:00.000+03:00","lastChangeTime": "2026-05-04T09:00:00.000+03:00","validThroughTime": "2026-06-04T09:00:00.000+03:00","isAdv": false,"searchUrl": "https://hh.ru/search/vacancy?text=python&area=1&page=0","searchSessionId": "00000000-0000-0000-0000-000000000000","company": {"id": 1000000,"name": "ООО «Пример»","isAccreditedIT": true,"isTrusted": true,"siteUrl": "https://www.example.com/","logoUrl": "https://hh.ru/employer-logo-original/0000000.png","badges": [{ "type": "hrbrand", "description": "Пример награды" }]},"salaryFrom": 200000,"salaryTo": 300000,"salaryCurrency": "RUR","salaryGross": false,"salaryMode": "MONTH","salaryFrequency": "TWICE_PER_MONTH","area": { "id": 1, "name": "Москва", "path": ".113.1." },"address": {"city": "Москва","street": "Тверская улица","lat": 55.7558,"lng": 37.6173,"metroStations": [{ "id": 1, "name": "Охотный Ряд", "lineColor": "#000000" }],"district": "Тверской район"},"workExperience": "between3And6","employment": "FULL","employmentForm": "FULL","workSchedule": "remote","workFormats": ["REMOTE"],"acceptHandicapped": false,"acceptLaborContract": true,"responsesCount": 42,"totalResponsesCount": 100,"onlineUsersCount": 5,"professionalRoleIds": [96, 156],"description": "<p>Обязанности: разработка и поддержка сервисов. Требования: опыт с Python 3+ года. Условия: удалённая работа, ДМС.</p>","descriptionText": "Обязанности: разработка и поддержка сервисов. Требования: опыт с Python 3+ года. Условия: удалённая работа, ДМС.","keySkills": ["Python", "Django", "PostgreSQL", "Docker", "Git"],"contactInfo": {"name": null,"email": null,"phones": [],"contactsHidden": false},"detailScraped": true,"scrapedAt": "2026-05-05T12:00:00.000Z"}
Plan Requirement
The prefilled proxy setting works out of the box. For geo-pinned results, such as region-specific salary surveys, the residential RU proxy group gives more headroom; pick it under Connection. hh.ru itself caps every search filter at 2000 results (40 pages of 50); slice a broad search into narrower ones to capture more, as described above.
FAQ
How much does it cost?
You pay per vacancy returned. The Pricing tab shows the current rates. Use Max total listings to cap the cost of any run.
Is it legal to scrape hh.ru?
This actor collects only publicly available job listing data. You are responsible for how you use it: follow hh.ru's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Contact details behind hh.ru's login wall are never collected; contactInfo is only populated when hh.ru itself shows it publicly.
Can I get only new or changed vacancies on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated and reappeared vacancies, and unchanged ones are not billed unless you turn on emitUnchanged.
Why does search mode only match vacancy titles by default?
searchField defaults to "name" so a keyword search stays relevant. hh.ru's own default behavior matches the keyword anywhere (title, description or company), which mixes in loosely related roles. Set "searchField": "everywhere" if you deliberately want that wider reach.
Why did my run fail instead of returning an empty dataset?
If hh.ru refuses every request, the run stops with a clear message so "no vacancies found" is never confused with "nothing could be read". Run it again in a few minutes, or try a different proxy configuration. A search that genuinely has no matches still finishes normally with an empty dataset. The run also fails with a clear message if no keywords, URLs or vacancy IDs were given, if the resume ID can't be read, or if you combine a resume ID with incremental mode that already has saved state.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
🔗 Want more jobs data?
Pair this actor with these related scrapers from the same team:
| 🏷️ Avito.ru Scraper From $1/1K. Scrape structured listings from Avito.ru by region, category, filters, or... | 💼 Totaljobs Scraper Scrape UK job listings from Totaljobs.com by keyword, location, filters, or URL. Extract... |
| 🏠 Domclick RU Property Scraper Extract property listings from domclick.ru, one of Russia’s largest real estate portals... | 💼 France Travail Scraper Scrape job offers from France Travail with titles, companies, locations, salaries... |
| 💼 Dice.com Scraper Scrape tech job listings from Dice.com by keyword, filters, or URL. Extract job titles... | 🛒 Ozon.ru Scraper Extract structured product data from Ozon.ru, Russia’s largest marketplace. Search by... |
💬 Support & custom scrapers
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email abotapi@proton.me or message Telegram @abotapi.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.