# Job Postings Scraper — fill-rate report (`rankfabrik/job-postings-scraper`) Actor

Search a job title and a place to get structured listings: title, company, location and a direct apply link on essentially every row. The real fill-rate of salary, applicant count and full description is measured and published for every run.

- **URL**: https://apify.com/rankfabrik/job-postings-scraper.md
- **Developed by:** [RankFabrik Team](https://apify.com/rankfabrik) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 job listing rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Job Listings Scraper — title, company, location and a direct link on every row

Give it a job title and a place, and it returns structured job listings ready
to work: a title, a company, a location and a direct link to apply on
essentially every row, because those are the fields the source itself always
publishes.

The fields that tell you whether a listing is worth pursuing (salary, applicant
count, a full description) are published unevenly, and most job scrapers only
let you find that out after the run is paid for. This one measures the real
fill-rate of those fields and puts it in front of you **before** you commit your
budget, so you never pay to discover a search was thin.

> **Unofficial tool — not affiliated.** This is an independent tool. It is not
> affiliated with, endorsed by, or sponsored by LinkedIn, Indeed, or any other
> third-party service. All product names, logos and brands are the property of
> their respective owners and are used for identification only.

***

### Source disclosed, no login scraped

This actor reads the public, logged-out job listing channel: no account, no
session, no protection bypassed. It accesses only what is already visible to any
anonymous visitor.

### You see the fill-rate before you pay, not after

Most scrapers work in the dark: you pay, you run, and only then learn how many
rows carry a salary or an applicant count. This one turns that around. Every
run reports the live fill-rate of `salary`, `url` and — with the full fiche on
— `applicantsCount`, in two places: the run's status message while it works,
and a `batchCompleteness` block on every single row. Start small, read the
real numbers for your exact search, and scale up only when they look worth it.
A search that returns nothing is never billed either — see *Pricing*.

***

### What you get

One row per job listing, deduplicated on the listing identifier when the
source provides one, on title + company + location otherwise:

| Field | Notes |
|---|---|
| `id` | source listing identifier, or `null` when the source omits it |
| `title` | job title |
| `company` | company name |
| `location` | raw location string as published |
| `city`, `region`, `country` | parsed from `location` — no extra request |
| `published` | publication date (ISO), when the source dates the card |
| `salary` | salary as published, or `null` — see *Known limits* |
| `url` | direct link to the listing |
| `logo` | company logo URL, or `null` |
| `enriched` | `true` when both `salary` and `url` are present |
| `batchCompleteness` | the completeness percentages of this run |

With **Fetch the full listing for each job** turned on, every row also gets:

| Field | Notes |
|---|---|
| `description` | full job description, plain text |
| `descriptionLength` | character count — a quick filter for thin postings |
| `seniority` | seniority level, as labeled by the source |
| `employmentType` | contract type (full-time, contract, internship…) |
| `jobFunction` | job function |
| `industries` | industry |
| `applicantsCount` | applicant count, or `null` — see *Known limits* |
| `applicantsCountCapped` | `true` when the source's own display cap was hit |

**No field is ever inferred.** If the source does not publish a salary, you get
a clean `null` — never a plausible-looking figure invented by a language model
to fill the gap. A made-up salary looks like data right up until it misleads a
hiring decision, so we would rather hand you an honest blank.

The `batchCompleteness` percentages mirror exactly what the source publishes,
not what we drop. A low or `0%` `salary` reading simply means the source does
not expose that field for those listings — and because you read it before you
pay, an empty field is never a surprise you discover after the run.

***

### Example output — one row, illustrative

```json
{
  "id": "4446697394",
  "title": "Full-Stack Developer",
  "company": "Example Inc",
  "location": "Austin, Texas, United States",
  "city": "Austin",
  "region": "Texas",
  "country": "United States",
  "published": "2026-08-10",
  "salary": null,
  "url": "https://www.linkedin.com/jobs/view/4446697394",
  "logo": null,
  "enriched": false,
  "batchCompleteness": {
    "title": 100,
    "company": 99,
    "location": 100,
    "salary": 9,
    "url": 100,
    "published": 87
  }
}
```

This row shows exactly what "no field is ever inferred" means in practice:
`salary` is `null` because the source didn't publish one for this listing:
not a guess, not a blank filled in. With **Fetch the full listing for each job**
on, this same row also carries `description`, `descriptionLength`, `seniority`,
`employmentType`, `jobFunction`, `industries`, `applicantsCount` and `applicantsCountCapped`.

***

### Good to know before you run it

Published up front, not buried in a changelog — so there are no surprises.

- **Salary is rarely published.** Most listings carry no salary field at all —
  that is a property of the source, not of the tool. `batchCompleteness.salary`
  tells you the real rate for your exact search, every run.
- **The source hard-stops at 1000 positions per query.** Beyond that, nothing
  is served, at all — segmenting the same search re-opens a fresh thousand.
  This actor does that for you: add cities in **Extra cities to sweep**, or
  turn on **Also sweep the 3 publish-date windows** to split by publish-date
  window (24 h / 7 d / 30 d) as well. Measured on one search: 929 listings on a
  single US query rose to 2,727 with the three windows added, and 5,028 with
  eight cities.
- **Applicant count is capped by the source at 200.** `applicantsCountCapped: true`
  tells you the real number is 200 or more, not exactly 200.
- **Full-listing mode costs one extra request per listing.** With
  **Fetch the full listing for each job** on, expect roughly double the run time
  for the same result count. The source also occasionally refuses an individual
  listing even when its card loaded fine; that row is returned without the extra
  fields rather than dropped.
- **An empty result is never charged.** If the source refuses the request or
  the search is genuinely empty, the run reports it and bills you nothing.

***

### Input

| Field | Required | Default | Meaning |
|---|---|---|---|
| `jobTitle` | yes | — | job title or keywords, in plain words |
| `location` | no | `United States` | country, region or city |
| `maxResults` | no | 100 | hard ceiling on delivered (and billed) rows |
| `fullDetails` | no | `false` | fetch description, seniority, contract type, applicant count |
| `datePosted` | no | any time | restrict to last 24 h / 7 d / 30 d |
| `remoteOnly` | no | all listings | remote-only |
| `experienceLevel` | no | all levels | seniority filter |
| `extraCities` | no | `[]` | extra cities to sweep, each one a fresh 1000-position budget |
| `sweepDateWindows` | no | `false` | also sweep all 3 publish-date windows, ×3 requests |

Example:

```json
{
  "jobTitle": "software engineer",
  "location": "United States",
  "maxResults": 300,
  "fullDetails": true,
  "datePosted": "r604800"
}
```

***

### Pricing

Pay per result, in two tiers, because not all rows are worth the same to you:

| Event | Price | What it is |
|---|---|---|
| `job` | $2.50 per 1,000 rows | a job listing row |
| `job-complete` | $4.00 per 1,000 rows | a row with **both** a published salary and a direct link |

You are charged per row written to your dataset, and for nothing else. Failed
runs, refused requests and empty searches cost you zero. Your own spending cap
is enforced by the platform — the run stops cleanly when it is reached,
mid-dataset, without burning compute you did not authorise.

***

### Prefer a hosted API with a flat monthly plan?

This actor bills per result on the Apify platform. If you'd rather call a
hosted endpoint with your own key, a flat monthly quota, and the same
completeness block on every response, the same data ships as a standalone API:

- **Jobs API** — built for volume past the 1,000-result-per-query wall this
  actor works around by sweeping cities and publish-date windows. See the full
  pricing breakdown: <https://rankfabrik.com/jobs-api-pricing>
- Sibling APIs on the same principle:
  [Places](https://rankfabrik.com/google-places-api-pricing) (local business
  data, one flat unit per enriched record) and
  [YouTube transcripts](https://rankfabrik.com/captions-api-pricing)
  (human-vs-auto caption transparency).

Both share one engine and report the same measured fill rate. Pick the billing
model that fits your workload.

***

### Legal & responsible use

**Your responsibility as the user.** You are solely responsible for how you use
the data this tool returns, including compliance with applicable laws, the
source's terms, and data-protection rules where they apply. Job listings are
mostly company, role and location data rather than personal data, but any
personal detail (a named recruiter, for instance) is yours to handle lawfully:
you act as the data controller for it. We provide a data-access tool and grant
no license over third-party content.

**Removal requests.** To have a specific record removed, contact
contact@rankfabrik.com. Justified requests are processed promptly (target:
within 30 days).

***

### Support

Have a question or hit a bad run? Open the actor's **Issues** tab and quote the
run. Every execution logs its completeness figures, so we both start from the
same numbers rather than from guesswork.

# Actor input Schema

## `jobTitle` (type: `string`):

What you're looking for, the way you'd type it into a search bar: "developer", "registered nurse", "marketing project manager".

## `location` (type: `string`):

Country, region or city, spelled out: "United States", "California", "Austin".

## `maxResults` (type: `integer`):

Hard ceiling on the number of rows delivered — and therefore billed. The source never serves more than 1000 positions per query; beyond that, add cities or turn on the publish-date windows to multiply requests rather than raising this number in vain.

## `fullDetails` (type: `boolean`):

Adds the description, seniority level, contract type, job function, industry and applicant count. Costs one extra request per listing — run time roughly doubles. Off by default for large volumes.

## `datePosted` (type: `string`):

Keep only listings posted recently.

## `remoteOnly` (type: `string`):

Restrict to listings advertised as remote.

## `experienceLevel` (type: `string`):

Restrict to one seniority level.

## `extraCities` (type: `array`):

Optional. Each added city re-runs the collection over that area and multiplies the requests — and therefore the completeness — accordingly. Leave empty for a single query on the "Location" field.

## `sweepDateWindows` (type: `boolean`):

On top of the requested window, queries the last 24 h, 7 days and 30 days separately, then merges without duplicates. Triples the number of requests. Measured: 929 → 2727 listings on the same search.

## Actor input object example

```json
{
  "jobTitle": "developer",
  "location": "United States",
  "maxResults": 100,
  "fullDetails": false,
  "datePosted": "",
  "remoteOnly": "",
  "experienceLevel": "",
  "extraCities": [],
  "sweepDateWindows": false
}
```

# Actor output Schema

## `results` (type: `string`):

All job listing rows written to the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "jobTitle": "developer",
    "location": "United States"
};

// Run the Actor and wait for it to finish
const run = await client.actor("rankfabrik/job-postings-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "jobTitle": "developer",
    "location": "United States",
}

# Run the Actor and wait for it to finish
run = client.actor("rankfabrik/job-postings-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "jobTitle": "developer",
  "location": "United States"
}' |
apify call rankfabrik/job-postings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rankfabrik/job-postings-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/m680dyBoPU0t80Z8u/builds/PAedp0C7cinXfNgrr/openapi.json
