HeadHunter RU $1💰 URL | Filters | Enriched Job Data
Pricing
from $1.00 / 1,000 results
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
Maintained by CommunityActor stats
1
Bookmarked
82
Total users
19
Monthly active users
7 days ago
Last modified
Categories
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.
| Field | Example |
|---|---|
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" |
salaryFrom | 200000 |
salaryTo | 300000 |
salaryCurrency | "RUR" |
salaryGross | false |
salaryMode | "MONTH" |
salaryFrequency | "TWICE_PER_MONTH" |
area.id | 1 |
area.name | "Москва" |
area.path | ".113.1." |
address.city | "Москва" |
address.street | "Sample Street" |
address.lat | 55.7558 |
address.lng | 37.6173 |
address.metroStations | [{ "name": "Sample Station", "lineColor": "#000000" }] |
address.district | "Sample District" |
company.id | 1000000 |
company.name | "Sample Company" |
company.visibleName | "Sample Company" |
company.isAccreditedIT | true |
company.isTrusted | true |
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"] |
nightShifts | false |
internship | false |
acceptHandicapped | false |
acceptLaborContract | true |
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.contactsHidden | false |
professionalRoleIds | [96, 156] |
responsesCount | 42 |
totalResponsesCount | 100 |
onlineUsersCount | 5 |
isAdv | false |
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
Search by keyword + area (recommended)
{"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}
URL mode multi-search
{"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 (
salaryMinwindows: 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
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | search | search, url, or vacancy |
queries | string[] | ["AI"] | Keywords (one search per keyword). Russian fine. |
searchField | enum | name | Where 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. |
areas | string[] | ["1"] | Area IDs. 1=Moscow, 2=St Petersburg, 113=Russia. Full list at https://api.hh.ru/areas |
experience | enum | any | any, noExperience, between1And3, between3And6, moreThan6 |
employment | enum[] | [] | Multi: full, part, project, volunteer, probation |
schedule | enum[] | [] | Multi: fullDay, shift, flexible, remote, flyInFlyOut |
salaryMin | integer | (none) | Minimum monthly RUB |
onlyWithSalary | boolean | false | Only listings with a published salary |
orderBy | enum | relevance | relevance, publication_time, salary_desc, salary_asc, distance |
professionalRole | string[] | [] | Numeric role IDs from https://api.hh.ru/professional_roles |
industry | string[] | [] | Numeric industry IDs from https://api.hh.ru/industries |
urls | string[] | (example URL) | Search URLs in URL mode. Filter fields ignored in this mode. |
vacancyInput | string[] | (example) | Vacancy URLs or bare IDs in Vacancy mode. Full card per entry; maxPages ignored. |
maxPages | integer | 2 | 1 to 40 (hh.ru hard cap). Search/URL modes only. |
maxListings | integer | 0 | 0 = unlimited |
fetchDetails | boolean | true | Adds description, keySkills, contact info, full company logos |
resumeFromRunId | string | (none) | Continue ONE specific interrupted run — see "Resume vs Incremental mode" below |
incrementalMode | boolean | false | Cross-run change detection for recurring/scheduled runs — adds changeType (NEW/UPDATED/UNCHANGED/REAPPEARED/EXPIRED) to every row. See below. |
stateKey | string | (auto) | Name a monitoring campaign, or leave empty to derive one automatically from mode + search/URL/vacancy settings |
emitUnchanged | boolean | false | Also return UNCHANGED vacancies — returns (and bills) extra rows |
emitExpired | boolean | false | Also return vacancies no longer found (EXPIRED) — only after a complete, uncapped, non-resumed scan; returns (and bills) extra rows |
proxy | object | Apify default datacenter | Apify 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.
- 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.
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+RUin the proxy field. Requires Starter plan or higher. - Pagination cap: hh.ru caps each filter at 2000 vacancies (40 pages of 50). Use multiple
queriesorareasto 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
maxPagesand 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": falseto scrape pure SERP data (~30 listings per second). KeepsvacancyId, salary, area, address, company basics, work format, but dropsdescription,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
detailScrapedboolean, and the run log printsDetail descriptions: X/Y fetched (Z%). If you ever see <100%, re-run with a smallermaxListingsor 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.