HH.ru Jobs Scraper - Salaries, Skills & 50+ Fields avatar

HH.ru Jobs Scraper - Salaries, Skills & 50+ Fields

Pricing

from $1.00 / 1,000 results

Go to Apify Store
HH.ru Jobs Scraper - Salaries, Skills & 50+ Fields

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

Abot API

Maintained by Community

Actor stats

4

Bookmarked

128

Total users

16

Monthly active users

8 days ago

Last modified

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.

FieldExample
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 / salaryFrequency200000 / 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 / acceptLaborContractfalse / false / false / true
description / descriptionTextfull 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)
detailScrapedtrue, whether the detail page (description, skills, contact info) actually arrived on this row
professionalRoleIds[96, 156]
responsesCount / totalResponsesCount / onlineUsersCount42 / 100 / 5
isAdvfalse, whether the listing is a paid/promoted placement
changeType / changedFields / firstSeenAt / lastSeenAtincremental mode only: "UPDATED" / ["salaryFrom"] / timestamps
searchUrl / searchSessionIdthe search that produced this row
scrapedAt"2026-05-05T12:00:00.000Z"

How to Use

  1. Pick a search mode: search (keywords, area and filters below), url (paste hh.ru search URLs already refined in a browser) or vacancy (paste specific vacancy URLs or IDs for a full detail card each, no search).
  2. 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.
  3. Turn fetchDetails on or off, and set Max total listings and Max pages to control run size and cost.
  4. 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 ApifyClient
client = 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 classified NEW, UPDATED (with changedFields), UNCHANGED (suppressed and not billed unless emitUnchanged is on), REAPPEARED, or EXPIRED (only after a run that scanned the whole tracked search, with no cap hit or resume, and only with emitExpired). stateKey names 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: false returns pure search-result fields (salary, area, address, company basics, work format) and leaves description, descriptionText, keySkills, contactInfo and validThroughTime empty (null or []).
  • Detail coverage at scale. Some detail pages are refused on the first try; the actor retries them automatically, detailScraped marks 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.

  1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to 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

ParameterTypeDefaultDescription
modestringsearchsearch, url, or vacancy.
queriesarray(none; prefilled AI)Keywords, one search per keyword. Search mode only.
searchFieldstringnameWhere the keyword is matched: name (title only), everywhere, company_name, or description.
areasarray["113"] (whole country)Area IDs (1=Moscow, 2=St Petersburg, 113=Russia). Search mode only.
experiencestringanyany, noExperience, between1And3, between3And6, or moreThan6.
employmentarray(none; all types)Multi-select: full, part, project, volunteer, probation.
schedulearray(none; all schedules)Multi-select: fullDay, shift, flexible, remote, flyInFlyOut.
salaryMininteger(none)Minimum monthly salary in RUB.
onlyWithSalarybooleanfalseOnly return listings that publish a salary.
orderBystringrelevancerelevance, publication_time, salary_desc, salary_asc, or distance.
professionalRolearray(none)Numeric professional role IDs.
industryarray(none)Numeric industry IDs.
urlsarray(none; prefilled sample)Search URLs. URL mode only; filters above are ignored.
vacancyInputarray(none; prefilled sample)Vacancy URLs or bare numeric IDs. Vacancy mode only.
maxPagesinteger2Result pages to walk per query or URL, 1 to 40. Search and URL modes only.
maxListingsinteger0Hard cap across the run (0 = unlimited).
fetchDetailsbooleantrueAdds description, key skills, contact info and full company logos.
resumeFromRunIdstring(none)Continue one interrupted run.
incrementalModebooleanfalseReturn only new and changed vacancies on recurring runs.
stateKeystring(none; derived automatically)Name or share an incremental-mode monitoring campaign.
emitUnchangedbooleanfalseAlso return (and bill) unchanged vacancies.
emitExpiredbooleanfalseAlso return (and bill) expired vacancies.
proxyobject(none; prefilled Apify Proxy, datacenter)Connection settings. Include it when calling from the API.
mcpConnectorsarray(none)Optional: send a summary of each vacancy to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which items are created.
maxNotifyListingsinteger50Cap 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.

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...

👉 Browse all abotapi scrapers

💬 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.