HeadHunter RU $1💰 URL | Filters | Enriched Job Data avatar

HeadHunter RU $1💰 URL | Filters | Enriched Job Data

Pricing

from $1.00 / 1,000 results

Go to Apify Store
HeadHunter RU $1💰 URL | Filters | Enriched Job Data

HeadHunter RU $1💰 URL | Filters | Enriched Job Data

From $1/1K. Scrape hh.ru job listings with 50+ fields including salary, experience, schedule, employment, and role. Search by filters or use URLs. Detail enrichment adds full descriptions, key skills, contact info, and employer logos.

Pricing

from $1.00 / 1,000 results

Rating

3.3

(3)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

1

Bookmarked

82

Total users

19

Monthly active users

7 days ago

Last modified

Share

hh.ru Jobs Scraper

Pull rich job listings from hh.ru (HeadHunter), Russia's largest job board, with 50+ fields per record. Filter by keywords, area, experience, schedule, employment, salary, and professional role; or paste search URLs directly. Detail-page enrichment adds full descriptions, key skills, contact info, and employer logos.

Why this scraper?

  • 50+ fields per vacancy, 4x richer than typical hh.ru scrapers. Includes keySkills, full HTML description, employer logos, GPS coordinates, metro stations, salary ranges (from/to + currency + gross/net), work formats, and contact info when public.
  • Three modes: pick keywords + area + filters, paste hh.ru search URLs as-is, or hand in a list of specific vacancy URLs / IDs and get the full detail card for each.
  • Concurrent detail-page enrichment with per-record streaming push: results show up in the dataset as they finish, mid-run aborts preserve partial progress.
  • Free-tier friendly: works on Apify default datacenter proxy, no residential plan needed.
  • Full filter coverage: search field (title-only / everywhere), experience, employment, schedule, salary range, only-with-salary, professional role, industry, sort order.
  • Forward-walking pagination that respects hh.ru's 40-page (2000-listing) hard cap per filter.

Data you get

Sample shape, values are illustrative placeholders, not from a live listing.

FieldExample
vacancyId"100000001"
url"https://hh.ru/vacancy/100000001"
name"Sample Job Title"
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"
salaryFrom200000
salaryTo300000
salaryCurrency"RUR"
salaryGrossfalse
salaryMode"MONTH"
salaryFrequency"TWICE_PER_MONTH"
area.id1
area.name"Москва"
area.path".113.1."
address.city"Москва"
address.street"Sample Street"
address.lat55.7558
address.lng37.6173
address.metroStations[{ "name": "Sample Station", "lineColor": "#000000" }]
address.district"Sample District"
company.id1000000
company.name"Sample Company"
company.visibleName"Sample Company"
company.isAccreditedITtrue
company.isTrustedtrue
company.siteUrl"https://www.example.com/"
company.logoUrl"https://hh.ru/employer-logo-original/0000000.png"
company.logos[{ "type": "ORIGINAL", "url": "..." }, ...]
company.badges[{ "type": "hrbrand", "description": "Sample badge" }]
workExperience"between1And3"
employmentForm"FULL"
employment"FULL"
workSchedule"fullDay"
workScheduleByDays["FIVE_ON_TWO_OFF"]
workFormats["ON_SITE"]
workingHours["HOURS_8"]
nightShiftsfalse
internshipfalse
acceptHandicappedfalse
acceptLaborContracttrue
description"<p>Full HTML description goes here...</p>"
descriptionText"Full plain-text description goes here..."
keySkills["Python", "Django", "PostgreSQL"]
contactInfo.name"Contact Name"
contactInfo.email"contact@example.com"
contactInfo.phones[{ "country": "7", "city": "495", "number": "0000000", "formatted": "7 495 0000000" }]
contactInfo.contactsHiddenfalse
professionalRoleIds[96, 156]
responsesCount42
totalResponsesCount100
onlineUsersCount5
isAdvfalse
searchUrl"https://hh.ru/search/vacancy?text=python&area=1&page=0"
searchSessionId"00000000-0000-0000-0000-000000000000"
scrapedAt"2026-05-05T12:00:00.000Z"

How to use

{
"mode": "search",
"queries": ["python"],
"areas": ["1"],
"maxPages": 5,
"maxListings": 100,
"fetchDetails": true
}

Search with filters (experience + remote + salary)

{
"mode": "search",
"queries": ["data scientist"],
"areas": ["1", "2"],
"experience": "between3And6",
"schedule": ["remote"],
"salaryMin": 200000,
"onlyWithSalary": true,
"orderBy": "salary_desc",
"maxPages": 10
}

URL mode (paste any hh.ru search URL)

{
"mode": "url",
"urls": [
"https://hh.ru/search/vacancy?text=AI%20engineer&area=1&experience=between1And3&schedule=remote&page=0"
],
"maxPages": 5,
"fetchDetails": true
}
{
"mode": "url",
"urls": [
"https://hh.ru/search/vacancy?text=python&area=1&page=0",
"https://hh.ru/search/vacancy?text=java&area=2&page=0"
],
"maxPages": 3
}

Vacancy mode (scrape specific vacancies by URL or ID)

Hand in an exact list of vacancies you care about and get one full detail card per entry — no search runs, so hh.ru's 2000-result cap never applies. Accepts full URLs, mobile URLs, or bare numeric IDs (mix freely; duplicates are removed).

{
"mode": "vacancy",
"vacancyInput": [
"https://hh.ru/vacancy/123456789",
"https://m.hh.ru/vacancy/123456790",
"123456791"
]
}

Every entry is fetched concurrently and always returns the full card (description, key skills, contact info, logos — fetchDetails is implied). A removed or archived vacancy is omitted from the dataset (you are never charged for it); the run log names each skipped ID with the reason, and the run status message summarises how many dropped off. maxListings still caps the total; maxPages is ignored in this mode.

Resume vs Incremental mode

Two different tools for two different jobs — pick the one that matches what you're actually doing:

Resume (resumeFromRunId) continues ONE specific interrupted run. Paste a previous run ID or dataset ID and this run skips vacancies already collected there, appending only new ones — use this after a timeout/crash, or to keep extending one large walk-all pull across several runs without re-scraping (and re-billing) what you already have.

{ "mode": "search", "queries": ["python"], "areas": ["1"], "maxPages": 40, "resumeFromRunId": "PREVIOUS_RUN_ID" }

Incremental mode (incrementalMode) is for scheduling this actor daily/weekly against the SAME search, URL list, or vacancy list and getting only what changed. The actor remembers its own state (keyed by mode + search/URL/vacancy settings, or your own stateKey) — no run ID to paste. The first run returns everything as NEW; later runs return only NEW, UPDATED, and REAPPEARED by default.

{ "mode": "search", "queries": ["python"], "areas": ["1"], "incrementalMode": true }

Every row gets a changeType: NEW (first time seen), UPDATED (content changed — check changedFields for exactly which keys), UNCHANGED (identical to last run — only returned when emitUnchanged is on), REAPPEARED (was gone, now back), or EXPIRED (tracked previously, not found this run — only returned when emitExpired is on, and only after a run that scanned the full tracked search with no cap hit and no resume). emitUnchanged and emitExpired are both off by default because they return — and bill — extra rows; turn them on only when you specifically need that signal. firstSeenAt/lastSeenAt track when a vacancy was first and most recently observed.

Both features can combine: incrementalMode + resumeFromRunId bootstraps a fresh incremental baseline from a prior resume-style pull's dataset (so nothing looks NEW again tomorrow) — but only on a genuinely first run for that state key; if incremental state already exists, the run fails fast rather than silently mixing two different baselines.

A note on the 2000-result limit

hh.ru itself serves at most 2000 results per search filter (40 pages × 50) — this is a server-side cap, not a limit of this scraper, and no page count can exceed it. When a search matches more than what's reachable, the run logs a warning telling you so. To capture everything beyond 2000, split one broad search into narrower slices that each stay under the cap, e.g.:

  • by area (["1", "2", "4", ...] instead of ["113"]),
  • by salary band (salaryMin windows: 0–100k, 100–200k, 200k+),
  • by professional role or industry,
  • or by recency (orderBy: "publication_time" run on a schedule).

Each slice gets its own fresh 2000-result budget, and results are de-duplicated by vacancyId within a run.

Input parameters

FieldTypeDefaultDescription
modestringsearchsearch, url, or vacancy
queriesstring[]["AI"]Keywords (one search per keyword). Russian fine.
searchFieldenumnameWhere the keyword is matched: name (vacancy title only — the default, gives relevant title matches for a job-title search), everywhere (title + description + company — hh.ru's broad default, mixes in loosely-related roles), company_name, description.
areasstring[]["1"]Area IDs. 1=Moscow, 2=St Petersburg, 113=Russia. Full list at https://api.hh.ru/areas
experienceenumanyany, noExperience, between1And3, between3And6, moreThan6
employmentenum[][]Multi: full, part, project, volunteer, probation
scheduleenum[][]Multi: fullDay, shift, flexible, remote, flyInFlyOut
salaryMininteger(none)Minimum monthly RUB
onlyWithSalarybooleanfalseOnly listings with a published salary
orderByenumrelevancerelevance, publication_time, salary_desc, salary_asc, distance
professionalRolestring[][]Numeric role IDs from https://api.hh.ru/professional_roles
industrystring[][]Numeric industry IDs from https://api.hh.ru/industries
urlsstring[](example URL)Search URLs in URL mode. Filter fields ignored in this mode.
vacancyInputstring[](example)Vacancy URLs or bare IDs in Vacancy mode. Full card per entry; maxPages ignored.
maxPagesinteger21 to 40 (hh.ru hard cap). Search/URL modes only.
maxListingsinteger00 = unlimited
fetchDetailsbooleantrueAdds description, keySkills, contact info, full company logos
resumeFromRunIdstring(none)Continue ONE specific interrupted run — see "Resume vs Incremental mode" below
incrementalModebooleanfalseCross-run change detection for recurring/scheduled runs — adds changeType (NEW/UPDATED/UNCHANGED/REAPPEARED/EXPIRED) to every row. See below.
stateKeystring(auto)Name a monitoring campaign, or leave empty to derive one automatically from mode + search/URL/vacancy settings
emitUnchangedbooleanfalseAlso return UNCHANGED vacancies — returns (and bills) extra rows
emitExpiredbooleanfalseAlso return vacancies no longer found (EXPIRED) — only after a complete, uncapped, non-resumed scan; returns (and bills) extra rows
proxyobjectApify default datacenterApify Proxy datacenter works fine; Residential RU optional

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.

Output example

Sample shape, values are illustrative placeholders, not from a live listing.

{
"vacancyId": "100000001",
"url": "https://hh.ru/vacancy/100000001",
"name": "Sample Senior Python Developer",
"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": "Sample Company",
"visibleName": "Sample Company",
"isAccreditedIT": true,
"isTrusted": true,
"category": "COMPANY",
"state": "APPROVED",
"siteUrl": "https://www.example.com/",
"logoUrl": "https://hh.ru/employer-logo-original/0000000.png",
"logos": [
{ "type": "ORIGINAL", "url": "https://hh.ru/employer-logo-original/0000000.png" },
{ "type": "vacancyPage", "url": "https://hh.ru/employer-logo/0000000.png" }
],
"badges": [{ "type": "hrbrand", "description": "Sample badge" }]
},
"salaryFrom": 200000,
"salaryTo": 300000,
"salaryCurrency": "RUR",
"salaryGross": false,
"salaryMode": "MONTH",
"salaryFrequency": "TWICE_PER_MONTH",
"area": { "id": 1, "name": "Москва", "path": ".113.1." },
"address": {
"city": "Москва",
"street": "Sample Street",
"building": "1",
"displayName": "Москва, Sample Street, 1",
"lat": 55.7558,
"lng": 37.6173,
"metroStations": [
{ "id": 1, "name": "Sample Station", "lineColor": "#000000" }
],
"district": "Sample District"
},
"workExperience": "between3And6",
"employment": "FULL",
"employmentForm": "FULL",
"workSchedule": "remote",
"workScheduleByDays": ["FIVE_ON_TWO_OFF"],
"workFormats": ["REMOTE"],
"workingHours": ["HOURS_8"],
"nightShifts": false,
"internship": false,
"responseLetterRequired": false,
"acceptHandicapped": false,
"acceptLaborContract": true,
"responsesCount": 42,
"totalResponsesCount": 100,
"onlineUsersCount": 5,
"professionalRoleIds": [96, 156],
"languages": [{ "id": "eng", "name": "English", "level": "b2" }],
"userLabels": [],
"description": "<p>Full HTML description text goes here. Includes responsibilities, requirements, and conditions.</p>",
"descriptionText": "Full plain-text description text goes here. Includes responsibilities, requirements, and conditions.",
"keySkills": ["Python", "Django", "PostgreSQL", "Docker", "Git"],
"contactInfo": {
"name": "Contact Name",
"email": "contact@example.com",
"phones": [
{ "country": "7", "city": "495", "number": "0000000", "comment": null, "formatted": "7 495 0000000" }
],
"contactsHidden": false
},
"scrapedAt": "2026-05-05T12:00:00.000Z"
}

Plan requirement

  • Free Apify plan works. The default proxy is Apify Proxy datacenter, which is included on every plan and passes hh.ru reliably.
  • Residential RU is optional. If you want geo-pinned results (e.g. for region-specific salary surveys), pick Residential + RU in the proxy field. Requires Starter plan or higher.
  • Pagination cap: hh.ru caps each filter at 2000 vacancies (40 pages of 50). Use multiple queries or areas to slice past the cap — see "A note on the 2000-result limit" above. The run warns you when a search exceeds what's reachable.
  • maxListings: 0 = unlimited. Bounded only by maxPages and the 2000-cap above.

Tips

  • Relevant title matches by default. Search mode matches your keyword against the vacancy title (searchField: "name"), so "Продуктовый аналитик" returns product analysts — not the marketing directors and sales managers hh.ru's own broad search mixes in. Want that wider reach (keyword anywhere in title, description, or company)? Set "searchField": "everywhere".
  • Russian keywords work. Pass "queries": ["программист"] directly.
  • Get the latest postings: set "orderBy": "publication_time" to walk newest-first.
  • Skip detail pages for speed: set "fetchDetails": false to scrape pure SERP data (~30 listings per second). Keeps vacancyId, salary, area, address, company basics, work format, but drops description, keySkills, contactInfo.
  • Detail coverage at scale: hh.ru rate-limits detail-page requests per IP. The actor detects the rate-limit ("light") page, retries blocked pages with backoff on fresh IPs, and runs a sequential recovery pass — so large runs keep full descriptions instead of silently dropping them. Every record carries a detailScraped boolean, and the run log prints Detail descriptions: X/Y fetched (Z%). If you ever see <100%, re-run with a smaller maxListings or switch to Residential RU proxy for the best coverage.
  • Quick fan-out across regions: pass multiple areas (e.g. ["1", "2", "4"] for Moscow + St Petersburg + Novosibirsk) to compare a single keyword across markets.