# Indeed & LinkedIn Jobs Scraper (`garonne/jobs-aggregator-bundle`) Actor

Scrapes job listings from Indeed and LinkedIn into one normalized, deduplicated dataset — never two rows for the same job posted on both sites. You are never charged for a run that delivers zero offers. An Indeed and LinkedIn jobs scraper for job search and recruiting data.

- **URL**: https://apify.com/garonne/jobs-aggregator-bundle.md
- **Developed by:** [matheo daney](https://apify.com/garonne) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.50 / 1,000 job offer delivereds

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

## Indeed & LinkedIn Jobs Scraper

Fetch job listings published on Indeed and LinkedIn for a given title and location in a single
run, already cleaned up and deduplicated — one row per listing, even when it was posted on both
sites at once.

### Why this Actor

- **One schema, two sites** — no need to run a separate Indeed scraper and LinkedIn scraper and
  then reconcile the results by hand: title, company, location (city/country), remote status,
  contract type, and salary (min/max/currency/period) arrive already normalized, regardless of
  the source site.
- **Never a billed duplicate** — the same listing posted on both Indeed and LinkedIn is delivered
  only once (the more complete of the two versions), with a record of every source it was found on.
- **You only pay for what's delivered, always at the same price** — no event is ever charged on a
  run that delivers zero usable offers, whatever the reason (source outage, an overly narrow
  search, etc.), and the price per offer never varies by source site. See "Pricing" below.

### Sample output (one dataset row)

```json
{
  "title": "Ingénieur Systèmes et Réseaux - DevOps H/F",
  "company_name": "Exemple Technologies SAS",
  "company_url": "https://fr.indeed.com/cmp/Exemple-Technologies",
  "location_city": "Nanterre",
  "location_country": "FR",
  "remote_type": "hybrid",
  "contract_type": "full_time",
  "salary_min": 55000,
  "salary_max": 60000,
  "salary_currency": "EUR",
  "salary_period": "year",
  "description_text": "Nous recherchons un(e) ingénieur systèmes et réseaux pour renforcer notre équipe technique. Poste en CDI à temps plein, télétravail partiel possible.",
  "job_url": "https://fr.indeed.com/viewjob?jk=aaaa1111bbbb2222",
  "apply_url": "https://careers.exemple-employeur.com/vacancy/devops-h-f",
  "published_at": "2026-09-04T05:00:00.000Z",
  "source_site": "indeed",
  "source_actor": "valig/indeed-jobs-scraper",
  "duplicate_of_sources": []
}
```

*(Real listing content is in whatever language the original posting used — this example reflects
a French job market listing, since Indeed/LinkedIn France is this Actor's current scope. Fields
`location_raw`, `salary_raw`, `scraped_at`, and `dedup_key` are omitted here for readability, but
present in the real dataset.)* A field that couldn't be determined for a given listing is always
explicit `null` — never a guessed value.

**Country format**: `location_country` is always an ISO 3166-1 alpha-2 code in UPPERCASE (e.g.
`"FR"`), regardless of which source found the listing — never a full country name ("France").

**No region field**: unlike city and country, none of the 3 sources behind this Actor (2 for
Indeed, 1 for LinkedIn) expose a usable region/state name in their actual data — so this dataset
does not include a `location_region` field. If you need a finer breakdown than city, you'll need
to derive it yourself from `location_city` or `description_text`.

**Salary**: `salary_min`/`salary_max` are rounded to the nearest whole currency unit (to the cent
for an hourly rate) and set to `null` if they fall outside a plausible range for their period, or
if their raw precision is absurd (more than 2 decimal places — a sign of an upstream conversion
artifact, never a salary actually displayed by an employer). An unusable salary is always `null`,
never a questionable value delivered as-is.

### Available filters

- **Job title / keyword** (`query`, required) — e.g. "react developer", "accountant".
- **Location** (`location`) — city, region, or country.
- **Remote only** (`remoteOnly`) — only keep `remote`/`hybrid` listings. Note: remote/hybrid
  status isn't always available from every source, so `remote_type` is sometimes `unknown` even
  for a listing that could genuinely be remote.
- **Contract types** (`contractTypes`) — full-time, part-time, internship, apprenticeship,
  freelance, temporary, volunteer.
- **Include LinkedIn** (`enableLinkedin`, **enabled by default**) — see the known limitation below.

### ⚠️ Actual scope — read before you buy

**2 sites, no more**: Indeed (via `valig/indeed-jobs-scraper` as the primary source, with an
automatic fallback to `borderline/indeed-scraper` if the primary is down) and LinkedIn (via
`crawlworks/linkedin-jobs-scraper`). This isn't "a lot of sources" — it's a common schema, real
deduplication, and an automatic fallback across 2 sites, not a promise of broader coverage.

**The price stays the same no matter which source found the listing** — we never pass a variable
sourcing cost on to your bill. As a trade-off, if Indeed's primary source is fully down when your
run starts, the automatic fallback may deliberately deliver nothing rather than run a more
expensive source to collect data: you'd then be charged $0 for that run (see "Pricing" below,
"no offer delivered = $0"), never a higher price to compensate.

**Known limitation of the LinkedIn source** (checked 2026-09-05, source's Store page): since
August 2026, LinkedIn's own Job Type / Experience Level / Work Type filters no longer work on
LinkedIn's side (it switched to AI-driven search) — result matching is done by that AI and **is
not guaranteed to be exact**. LinkedIn is included by default (this Actor's value is combining
both sites, not scraping Indeed alone) — a LinkedIn outage never fails the run either way. Turn it
off (`enableLinkedin: false`) if you'd rather use Indeed only.

**Remote status isn't always known**: `remote_type` reflects what each source actually exposes.
Indeed's own listings often don't state remote/hybrid status explicitly, and LinkedIn doesn't
expose a dedicated field for it either — in both cases this Actor falls back to scanning the job
description text, which is not as reliable as a dedicated field. Expect a meaningful share of
listings to come back as `remote_type: "unknown"` rather than a guess.

### Pricing

| Unique offers delivered | Price |
|---|---|
| 1 offer | $0.0035 |
| **1,000 offers** | **$3.50** |
| 10,000 offers | $35.00 |

**No offer delivered = $0.** A run that finds nothing usable (an overly narrow search, upstream
sources down, etc.) charges nothing at all — never a fixed start-up fee, never a charge for a
failed run.

### Configuration

| Field | Type | Description |
|---|---|---|
| `query` | string (required) | Job title or keyword. |
| `location` | string | City, region, or country. |
| `enableLinkedin` | boolean (default `true`) | Include LinkedIn (see the limitation above). |
| `maxItemsPerSource` | integer (default `100`) | Cap on results requested from EACH source — protects your own bill, independently of the price above. |
| `upstreamTimeoutSecs` | integer (default `120`) | Delay before a source is considered down (triggers the automatic Indeed fallback). |
| `remoteOnly` | boolean (default `false`) | Filter applied after normalization. |
| `contractTypes` | array (default empty = all) | Filter applied after normalization. |

See `.actor/INPUT_SCHEMA.json` for the full field list and `.actor/dataset_schema.json` for the
complete output schema.

# Actor input Schema

## `query` (type: `string`):

Search term used across all sources (e.g. "react developer", "accountant").

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

City, region, or country. Optional.

## `enableLinkedin` (type: `boolean`):

Adds LinkedIn results alongside Indeed. Enabled by default — this Actor's value is aggregating both sites into one deduplicated dataset, not scraping Indeed alone. Known limitation: since August 2026, LinkedIn's own Job Type / Experience Level / Work Type filters are unreliable (LinkedIn moved to AI-driven matching, not guaranteed to be exact) — this doesn't affect Indeed results, and a LinkedIn outage never fails the run. Uncheck to use Indeed only (with automatic fallback to a secondary Indeed source if the primary one is unavailable).

## `maxItemsPerSource` (type: `integer`):

Maximum number of results requested from EACH source. Caps your own upstream usage cost — each source is billed to your Apify account independently of this Actor's price.

## `upstreamTimeoutSecs` (type: `integer`):

Server-side timeout applied to each source. For Indeed, a timeout triggers an automatic fallback to a secondary source. For LinkedIn, a timeout is recorded as a failure (no fallback available for this source).

## `remoteOnly` (type: `boolean`):

Only keep listings marked as remote or hybrid after normalization. Note: remote/hybrid status isn't always available from every source, so some genuinely remote listings may be excluded if the source didn't report it.

## `contractTypes` (type: `array`):

Filter applied after normalization (each source's own wording is mapped to this common list). Leave empty to include all types.

## Actor input object example

```json
{
  "query": "developer",
  "location": "Paris",
  "enableLinkedin": true,
  "maxItemsPerSource": 100,
  "upstreamTimeoutSecs": 120,
  "remoteOnly": false,
  "contractTypes": []
}
```

# Actor output Schema

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

One row per unique, normalized job listing.

# 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 = {
    "query": "developer",
    "location": "Paris"
};

// Run the Actor and wait for it to finish
const run = await client.actor("garonne/jobs-aggregator-bundle").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 = {
    "query": "developer",
    "location": "Paris",
}

# Run the Actor and wait for it to finish
run = client.actor("garonne/jobs-aggregator-bundle").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 '{
  "query": "developer",
  "location": "Paris"
}' |
apify call garonne/jobs-aggregator-bundle --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,garonne/jobs-aggregator-bundle"
        }
    }
}

```

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/3eXb39q4f7l5VDRfi/builds/1QkPWtYCORlwFz3zA/openapi.json
