Job Posting Monitor
Pricing
from $17.00 / 1,000 results
Job Posting Monitor
Finds companies hiring for your keywords on Google Jobs (with your SerpAPI key) and free job boards. One flat Clay-ready row per posting with company domain and LinkedIn URL. Optional: the full job description, a stable posting_key, and a row when a posting closes or is reposted.
Pricing
from $17.00 / 1,000 results
Rating
0.0
(0)
Developer
Mamba Labs
Maintained by CommunityActor stats
0
Bookmarked
5
Total users
3
Monthly active users
7 hours ago
Last modified
Categories
Share
📋 What can Job Posting Monitor do?
Give it role keywords and it returns the companies hiring for that role right now, one clean row per posting, with each company enriched with its domain, LinkedIn URL, employee count and industry.
This is the reverse of a domain-level ATS checker. You do not give it a company and ask "are they hiring?" You give it a role and ask "who is hiring for this?"
| 📦 What you get | ⚙️ Features and integrations |
|---|---|
| 🏢 Hiring companies, deduplicated to one row per posting 🔗 Domain and LinkedIn URL resolved per company 🧹 Staffing agencies and freelance marketplaces filtered out 🧾 43 flat fields, snake_case, Clay-native | 🔍 Google Jobs discovery via your own SerpAPI key 🏷️ Company type classification on every row 🔄 Cross-run delta detection, only new postings ⬇️ Export to JSON, CSV, Excel, HTML or XML |
Bought by anyone running role-triggered outbound: recruiters, staffing-adjacent vendors, and sellers whose buying signal is a specific role opening.
🚫 The keyword list is fully generic, and the editorial default is just a preset. It ships with nine editorial and content titles (
proofreader,copy editor,content editor,copywriter,content writer,technical writer,editorial,content marketing,content strategist) because that was the first use case, but the same actor tracks engineering, sales, marketing, operations or finance by changing the keywords. The Input tab prefill runs that preset on three keyless boards (Jobicy, Remotive and Himalayas), so it returns real postings with no key.
💡 Why use Job Posting Monitor?
| If you want | Read these fields |
|---|---|
| The hiring company | company_name, company_domain |
| An outreach target | company_linkedin_url |
| To size the account | employee_count, industry |
| To drop the wrong kind of company | company_type |
| The role and where to apply | job_title, job_url, location, is_remote |
| Recency and novelty | posting_date, is_new_posting |
| Why this row matched | matched_keyword, job_description_snippet |
🧹 Filtering out the noise is most of the value
Raw job search results are dominated by staffing agencies and freelance marketplaces reposting the same roles. This actor filters both.
Staffing and recruitment agencies are filtered on both the company name and the job description text, because plenty of agencies do not have "recruitment" in their name. Freelance marketplaces (Upwork, Fiverr, Freelancer, Toptal and similar) are excluded by name, and the list is yours to extend.
Everything that survives is classified: direct_employer, staffing_agency, government or
unknown. If you disagree with the filter, turn it off and filter on company_type yourself.
🔄 Re-runs only return what is new
is_new_posting and the built-in delta cache mean a scheduled run does not re-emit everything it
already showed you. Pass previous_run_date if you would rather control the window yourself.
📋 What data can Job Posting Monitor extract?
16 fields per posting.
| Field | What it holds |
|---|---|
company_name | The hiring company |
company_domain | Resolved canonical domain, or null |
company_linkedin_url | Company LinkedIn page, the primary outreach field |
employee_count, industry | Firmographics, where the company exposes them |
company_type | direct_employer, staffing_agency, government or unknown |
job_title | The role title |
job_url | Direct apply or posting URL |
posting_date | Normalized to ISO, best effort |
source_board | google_jobs, greenhouse, lever or ashby |
location, is_remote | Where the role is |
job_description_snippet | First 500 characters of the description |
matched_keyword | Which of your keywords this posting matched |
is_new_posting | true when not seen in a prior run |
run_date | When the run happened |
⚠️
employee_countandindustryare frequently null, because the firmographic source only reports them when a company exposes them. Thecompany_size_minandcompany_size_maxfilters therefore apply only when a size is known, and will silently keep companies of unknown size.company_linkedin_url, the field most people actually act on, resolves for the large majority.
🛠️ How to find companies hiring for a role
- Open the Input tab and put your role titles in
keywords. - Paste your own SerpAPI key into
serpapi_key. Google Jobs runs only with it. - Set
countryandlookback_daysto scope the search. - Leave the staffing and freelance filters on.
- Click Start, then work the rows where
company_typeisdirect_employer.
🧪 Using it in Clay
Point a Clay table at this actor's dataset, one Clay row per output row. The key columns are
company_name, company_domain, company_linkedin_url, job_title and posting_date.
Use company_linkedin_url as the input to a LinkedIn step, and posting_date plus is_new_posting
to prioritize fresh signals.
Schedule the actor and rely on the built-in delta cache, or pass previous_run_date, so each refresh
only ingests new postings.
🌐 Choosing your job board sources
sources selects which boards a run searches. The default is google_jobs, which is what every
earlier version did, so an existing saved input behaves exactly as it did before.
Sources run as an ordered chain, cheapest first, not all at once:
| Tier | Source | Key needed | Billed per | What it covers |
|---|---|---|---|---|
| 0 | jobicy | none | nothing | Remote roles, marketing, writing, design and support heavy. Searched by tag, one request per keyword (up to 8); when every tag search is empty, the newest 50 postings are read instead. |
| 0 | remotive | none | nothing | Remote roles, keyword searchable at the source. |
| 0 | arbeitnow | none | nothing | Europe heavy, strong on Germany and the Netherlands. |
| 0 | themuse | none | nothing | Mid market and enterprise employers in the United States. |
| 0 | remoteok | none | nothing | The newest 100 remote postings on Remote OK. |
| 0 | weworkremotely | none | nothing | The We Work Remotely feed, remote only. |
| 0 | himalayas | none | nothing | The newest pages of Himalayas, remote only. |
| 1 | adzuna | Adzuna app id and key | call | Indeed style aggregate coverage per country, dated at the source. |
| 1 | indeed | private testing | not available | Indeed search results. In private testing and not yet available to public accounts; see below. |
| 2 | jobspipe | JobsPipe key | job returned | 30 plus boards normalized, with the employer domain on the row. |
| 2 | google_jobs | SerpAPI key | search | The widest corpus: Indeed, LinkedIn, Glassdoor, ZipRecruiter and career pages. |
| 2 | theirstack | TheirStack key | job returned | Career sites and boards, with employer domain, LinkedIn URL and headcount on the row. |
Tier 0 runs every keyword. Tier 1 runs only on keywords Tier 0 left short, and Tier 2 only on
keywords still short after that, JobsPipe first and TheirStack last because its allowance is the
smallest. A keyword is satisfied once the run has found enough_new_companies_per_keyword distinct
new companies for it (default 3); when every keyword is satisfied the chain ends whatever budget is
left. Between tiers every posting is checked against the cross-run delta cache, so a metered source
is never asked for a posting the run already has.
Metered queries are narrowed to metered_window_days (default 2, today and yesterday). JobsPipe
and TheirStack bill per job returned whether or not you have seen it before, and the delta cache
dedupes after the bill, so the date window is what keeps those credits.
A posting that shows up on two boards is deduplicated on company plus normalized job title. The run
log reports the raw count, the new count and the calls and credits spent per source and per country,
and the same table is written to the run's key value store as RUN_SUMMARY.
countries loops several countries inside one run. Metered sources query each country separately;
the Tier 0 boards are global remote boards and run once. Use the names Google Jobs accepts: United
States, United Kingdom, Canada.
🖥️ Indeed (private testing)
Indeed is in private testing and is not yet available to public accounts. If you add indeed to
sources, the run skips it, every other source runs as normal, and RUN_SUMMARY sets
degraded: true with the reason indeed is in private testing and is not available on this account.
No browser starts and nothing extra is charged.
The seven Tier 0 boards need no credentials of any kind, so a run with sources: ["jobicy"] works on
a fresh account with nothing configured.
🔑 The keys
Google Jobs runs only on your own SerpAPI key, pasted into serpapi_key (stored encrypted).
The actor holds no SerpAPI key of its own. Without one, Google Jobs is skipped, and the run log,
the run status message, and degradation_reason in RUN_SUMMARY say so (and the summary row,
when no posting row comes back). The Tier 0 boards still run.
The other metered sources work the same way: each runs only on your own key from the input
(adzuna_app_id and adzuna_app_key, jobspipe_key, theirstack_key). The actor holds no
provider key of its own. Without a key, that source is skipped, and the run log and
degradation_reason in RUN_SUMMARY say so.
💵 How much does it cost to find job postings?
You are charged once per new posting row returned. A re-run that finds nothing new returns no rows and charges no result.
| Plan | Per posting | Per 1,000 postings |
|---|---|---|
| Free | $0.02 | $20.00 |
| Bronze | $0.019 | $19.00 |
| Silver | $0.018 | $18.00 |
| Gold | $0.017 | $17.00 |
There is also an Actor start event at $0.00005, charged once per run per GB of memory.
Free Apify plans get 25 results per calendar month, resetting on the 1st. Paid plans have no monthly cap. On a free plan, company enrichment is also capped at 2 companies per run.
🛑 Your spending limit is respected. Set a maximum cost per run in Apify, or max_charge_usd on the Input tab, and the run stops before it would go over, keeping every result already charged. Each row's billable_events_json lists the events that row was charged for.
💳 Enrichment runs other Mamba Labs actors on your account. Company enrichment calls Company Identity Resolver, Company Firmographic Enricher, and Domain to LinkedIn URL Resolver with the token that started the run, so their per result prices are billed to you as well.
max_companiesbounds how many companies are enriched. Metered job sources (SerpAPI, Adzuna, JobsPipe, TheirStack) bill the key you supply, not Apify.
📅 Scheduled pricing change, 2026-10-26 12:00 UTC. Results move from Apify's default per-row event to a named
resultevent at the same price on every plan, so each result row costs exactly what it costs today. A run summary row and a blocked lookup row stop being charged. Until then every row is billed as described above.
⌨️ Input
Everything is on the Input tab. The options worth explaining:
| Field | Type | Default | What it does |
|---|---|---|---|
keywords | array | editorial preset | Role titles to search for. Any role works. |
sources | array | ["google_jobs"] | Which job boards to search, run as the chain above. |
serpapi_key | string (secret) | none | Your own SerpAPI key for google_jobs. Without it Google Jobs is skipped and the run says so. |
adzuna_app_id, adzuna_app_key | string | none | Your Adzuna credentials for adzuna. |
jobspipe_key, theirstack_key | string | none | Your JobsPipe and TheirStack keys. |
country | string | United States | Geographic filter for one country. |
countries | array | none | Several countries in one run; overrides country. |
metered_window_days | integer | 2 | Date window for metered queries, 1 to 7. |
enough_new_companies_per_keyword | integer | 3 | New companies that satisfy a keyword and stop its chain. |
metered_run_cap | integer | 50 | Ceiling per metered source per country per run. |
indeed_countries | array | all | Limit Indeed to these countries. Indeed is in private testing. |
source_time_budgets_secs | object | none | Override a source's time window in seconds, for example {"indeed": 5}. A source that runs out of its window is retried once, then stopped, and the run moves on. Google Jobs always gets its turn. |
persist_delta | boolean | true | Remember returned postings as seen. Turn off for a test run so the next run still returns them. |
lookback_days | integer | 30 | Only return postings newer than this. |
company_size_min, company_size_max | integer | none | Drop companies outside the range, when size is known. |
exclude_staffing_agencies | boolean | true | Filter staffing and recruitment postings. |
exclude_freelance_marketplaces | boolean | true | Filter Upwork, Fiverr and similar. |
freelance_marketplaces | array | 7 defaults | Override or extend the marketplace list. |
remote_only | boolean | false | Only return remote postings. |
previous_run_date | string | none | ISO date; only emit postings newer than this. |
max_results | integer | 100 | Raw postings scanned before filtering. |
max_companies | integer | 40 | Cap on unique companies enriched per run. Bounds sub-actor cost. |
track_postings | boolean | false | Keep a posting history in your own account, fill posting_key, first_seen_at, last_seen_at, and change_type, and return closed and reposted postings. See below. |
emit_closed | boolean | true | With tracking on, return one row per closed posting. Off records closures without returning them. |
closed_after_misses | integer | 2 | With tracking on, complete runs in a row without the posting before it counts as closed. |
closed_after_days | integer | 30 | With tracking on, days not seen before a posting counts as closed. |
state_key | string | derived | With tracking on, names this monitor's history record. Empty derives it from the keywords, sources, and countries. |
mode | string | gtm | gtm finds companies hiring for a role. job_seeker finds local postings for a person looking for work; see Job seeker mode below. |
location | string | none | Job seeker mode: the city to search around, for example Phoenix, Arizona, United States. |
radius_km | integer | none | Job seeker mode: search radius in kilometers, sent to Google Jobs, which treats it as a hint. |
employment_types | array | none | Keep only full_time, part_time, contract, temporary or internship postings. A posting with no schedule type is kept. |
exclude_keywords | array | none | Drop a posting when any term appears in its title or description. Whole word, case insensitive. |
pages_per_keyword | integer | 1 in job seeker mode | Google Jobs pages of up to 10 results per keyword, 1 to 5. Each page is one SerpAPI search. |
known_job_ids | array | none | Job seeker mode only. Google job ids you already hold. Paging for a keyword stops at the first page that brings no id outside this list and the ids already seen in the run. RUN_SUMMARY.job_seeker.pages lists every page searched. |
include_full_description | boolean | false | Return job_description_full. Always on in job seeker mode. |
max_charge_usd | number | none | Optional. The run stops before the next result that would take this run's charges over this amount, and keeps every result already charged. Leave empty to rely only on the maximum cost per run you set in Apify. |
📤 Output
One flat row per posting, exportable as JSON, CSV, Excel, HTML or XML.
{"company_name": "Stripe","company_domain": "stripe.com","company_linkedin_url": "https://www.linkedin.com/company/stripe","employee_count": 8000,"industry": "Financial Services","company_type": "direct_employer","job_title": "Senior Software Engineer","job_url": "https://boards.greenhouse.io/example/jobs/123","posting_date": "2026-06-25","source_board": "google_jobs","location": "San Francisco, CA","is_remote": true,"job_description_snippet": "We are seeking a backend engineer to...","matched_keyword": "software engineer","is_new_posting": true,"posted_via": "LinkedIn","search_country": "US","degraded": false,"degradation_reason": null,"run_date": "2026-07-01T14:06:22.297Z"}
search_country is the country a metered query ran for, null for a global remote board. degraded
is true, with a plain reason, on a row whose enrichment did not finish inside its time budget: a null
company_domain on such a row means unresolved, not absent. Identity, firmographics and LinkedIn
each have their own budget, and LinkedIn runs last, so a slow LinkedIn lookup can cost a LinkedIn URL
and nothing else.
source_board is the source you selected. posted_via is the board the posting was actually
published through, as reported by that source, so you can see the board distribution behind a
google_jobs run without reconstructing it from the URLs.
🔁 Posting tracking
Off by default. With it off, every row is the same as in earlier builds, and the seven
columns below are null. The full posting description is job_description_full (see the job seeker
fields above).
Tracking (track_postings). The actor keeps one record per monitor in a named storage,
job-posting-monitor-state, in your own Apify account. Each row then carries:
| Field | What it holds |
|---|---|
posting_key | Stable key for the posting: pk_ plus 20 hex characters from the company domain, normalized title, and normalized location. The same key the other Mamba Labs hiring actors use. Null when the company domain did not resolve |
first_seen_at, last_seen_at | When the monitor first returned the posting, and when a run last saw it |
change_type | new, closed, or reposted |
closed_at | When the run detected the closure. Closed rows only |
closed_reason | ats_gone, missed_runs, or not_seen_days. Closed rows only |
closed_confidence | high, medium, or low. Closed rows only |
How a posting is detected as closed, in this order:
ats_gone, high confidence. When the posting's apply link is on Greenhouse, Lever, SmartRecruiters, Workable, Recruitee, Personio, or Workday, the actor asks that board whether the posting still exists. Only a 404 or 410 closes it. A refusal or a timeout never does. Links on LinkedIn, Indeed, and other aggregators are never checked, because a dead posting there often still answers.missed_runs, medium confidence. The posting was missing fromclosed_after_missesruns in a row. A run counts as a miss only when every search that ever returned the posting ran and read its results to the end. Google Jobs keywords that end their pagination, and keyless boards read without truncation, can be complete. Adzuna, JobsPipe, TheirStack, and Indeed read a date window or one page, so their searches never count a miss. Expect most closures to come from the board check or from age.not_seen_days, low confidence. Not seen forclosed_after_days.
A closed posting that comes back is returned once with change_type reposted. Closed and reposted
rows are results and are billed like any other result row. persist_delta false also leaves the
tracking record unwritten. Closed entries are dropped after 30 days and open entries after 90 days
unseen.
🧾 More fields on every row
| Field | Type | What it holds |
|---|---|---|
degradation_reason | string | Why the run was degraded, in plain language, or null when it was not. |
row_status | string | ok, degraded, error, or notice (a notice row carries a message, not a result). Present on every row. |
error_reason | string | Why the row is degraded or an error, in plain words. Null on an ok row. Present on every row. |
billable_events_json | string | JSON list of the pricing events this row was charged for, for example [{"event":"apify-default-dataset-item","count":1}]. Lets you reconcile a run from the dataset alone. The once per run start event is not repeated on rows. |
💵 Pay, schedule and apply fields
Every row carries these columns. Google Jobs is the source that fills them; every other source leaves them null.
| Field | Type | What it holds |
|---|---|---|
salary_text | string | Pay as the posting shows it, for example $17 to $19 an hour. |
salary_min, salary_max | number | Pay figures as posted, in the posting's own period. |
salary_period | string | hour, day, week, month or year. |
salary_annual_min, salary_annual_max | number | The same pay as a yearly figure: hour x 2080, day x 260, week x 52, month x 12. |
employment_type | string | full_time, part_time, contract, temporary or internship. |
schedule_text | string | The schedule type as Google Jobs shows it, for example Part-time. |
job_description_full | string | The full description. Filled in job seeker mode or with include_full_description. |
qualifications, benefits | array | Lines from the Google Jobs highlights. |
apply_options | array | Every apply link Google Jobs lists, as {title, link}. |
google_job_id | string | The Google Jobs id, a stable key for deduplication. |
Pay is read from the pay string Google Jobs shows, then from a pay sentence in the description. A figure without a pay period in the description (a sign on bonus, a weight limit) is never read as pay.
🧭 Job seeker mode
Set mode to job_seeker to search for work near one place instead of for companies that are hiring. The run searches Google Jobs only, around location, keeps every posting (several stores of one employer are several rows), skips LinkedIn resolution, keeps firmographics for up to max_companies companies, and returns the full description. Staffing agency and freelance marketplace filters stay on. Job seeker runs keep their own record of postings already returned, separate from hiring signal runs.
💡 Tips
- Work
company_type: "direct_employer"first. It is the reason the filtering exists. - Tighten
lookback_daysbefore raisingmax_results. Fresh postings convert better than deep ones. max_companiesis your cost control. It bounds how much enrichment a run can trigger.- Schedule it and filter on
is_new_postingto turn this into a standing feed rather than a one-time list. - Extend
freelance_marketplaceswith whatever else clutters your vertical. - No SerpAPI key yet? Start with
sources: ["jobicy", "remotive"]. No credentials needed. - Group
posted_viain your export to see which boards actually carry your role.
⚠️ Known limits
Discovery depth is bounded by max_results. Very high-volume keywords may need multiple runs or a
tighter lookback_days.
Company enrichment is capped per run by max_companies, deliberately, to bound sub-actor cost.
Enrichment depends on name-to-domain resolution. Ambiguous or very small companies may not resolve, and those rows come back with a null domain and null firmographics rather than being dropped.
employee_count and industry coverage varies and is frequently null. The size filters apply
only when a size is known.
Google Jobs coverage excludes some regions where the widget is disabled, including the EU.
Posting dates from aggregated boards are often relative, for example "25 days ago", and are normalized to a best-effort ISO date.
indeed is in private testing. It is not yet available to public accounts. A run that lists it
skips it and says so.
google_jobs needs a SerpAPI key. Without one, that source is skipped and the run says so. The
seven Tier 0 boards need no key.
Keywords match as a phrase inside the title or description. A bare writer also matches
"Underwriter", and a bare editor matches "Video Editor", so use the specific title.
❓ FAQ
Do I need a SerpAPI key?
Only for google_jobs. The seven Tier 0 boards (Jobicy, Remotive, Arbeitnow, The Muse, Remote OK,
We Work Remotely and Himalayas) need no key, and the Input tab prefill uses three of them. SerpAPI's
free tier covers 250 searches per month if you want Google Jobs as well.
Why did a run return nothing?
Usually no key, a keyword and country combination the selected source does not serve, or nothing new since your last run (the delta cache). The run sets a status message saying which, emits zero rows and exits cleanly rather than erroring.
The run's RUN_SUMMARY record (Storage, Key value store) labels the outcome: run_outcome is
no_results, blocked or timed_out, with run_outcome_reason, the sources that answered
(sources_reached), the sources that refused (sources_blocked), and the run ID and build. A board
that refused is labeled blocked; it is never reported as a board with zero postings.
A run that finds nothing costs nothing. Earlier versions pushed a single explanatory row when there were no new postings, and because a result is a dataset row, that row was billed as a result. It is not any more: those notices are status messages and log lines. If you have automation that reads a "no new postings" row, read the run status message instead.
How long does a run take, and what if it runs out of time?
Every request and every phase is budgeted against the run's own timeout, less a 90 second reserve.
Discovery gets at most half of the time left, then identity, firmographic and LinkedIn enrichment
each get their own share. A phase that does not finish keeps what it found, and the affected rows
say so in degraded and degradation_reason. A run on a short timeout ends with the rows it has
instead of being stopped by the platform with none.
Are staffing agencies really filtered out?
They are filtered on both company name and job description text, and anything that survives is
labeled in company_type. If you would rather judge for yourself, turn the filter off and filter on
that field.
Will a scheduled run keep showing me the same postings?
No. Cross-run delta detection means re-runs only emit postings you have not seen. is_new_posting
carries the flag.
What happens if the enrichment sub-actors fail?
The row is still emitted with null enrichment fields and a structured error is logged. Discovery is never lost to an enrichment failure.
🧩 Want other GTM data?
Mamba Labs builds custom actors for B2B go-to-market teams. The public versions of that work live here on the Store, so our users get the same tooling we build under contract.
Every actor in the suite takes a domain or a company and returns one flat row, so they stack in the same Clay table without reshaping anything.
🛠️ Need something custom built for you or your team? Tell us what you are trying to find and we will build it. Talk to Mamba Labs.
🆘 Support
Something wrong, or a company misclassified? Open an issue on the Issues tab with the keyword and the row, and we will look at it.
ℹ️ Sourcing and legal. Job postings come from the SerpAPI
google_jobsengine against your own key, and from public job board APIs. Company enrichment comes from Mamba Labs fleet actors reading public company data. The actor stores no personal data: it returns companies, roles and postings, not people. You are responsible for how you use the output.
Built by Mamba Labs.