# Jora Jobs Scraper - Jobs, Salaries, Companies & Locations (`abotapi/jora-jobs-scraper`) Actor

Scrape Jora.com job listings across supported countries by keyword, filters, or URL. Extract full job descriptions, salaries, work types, companies, locations, and structured data ready for connector export.

- **URL**: https://apify.com/abotapi/jora-jobs-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 dataset items

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Jora Jobs Scraper: Search, Job Details, and Recurring Change Monitoring

Jora Jobs Scraper turns Jora into structured job data. Search by keyword and location, or start from a list of Jora search or job URLs, across Australia, New Zealand, Singapore, Hong Kong, Malaysia, Indonesia, the United States, the United Kingdom, and Canada. Detail mode adds the full job description, apply link, salary, work type, and work arrangement for every listing. Run it once for a snapshot, or turn on incremental mode to get only new, updated, and reappeared jobs on a recurring schedule, then export to JSON, CSV, or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Nine country sites.** Search by keywords and location across nine Jora country sites, or start from a saved list of Jora search or job URLs.
- **Server side filters.** Server side filters for listed date, job type, quick apply, sort order, distance, and minimum salary, applied directly on Jora's own search pages (search mode; in URL mode the filters already in your pasted link apply).
- **Full details.** Detail mode adds the full job description, apply link, salary, work type, and work arrangement for every listing.
- **Bounded or full walks.** Max items is the main output cap; Max pages is unlimited by default, so a search walks every result page until Jora's own end of results.
- **Resume.** Resume a large pull across separate runs, or let an interrupted run pick back up automatically without re-saving jobs it already collected.
- **Change feed.** Incremental mode turns a recurring search into a change feed: only new, updated, and reappeared jobs come back on later scheduled runs.
- **MCP export.** Optional MCP connector export sends a condensed job summary into the apps you already use, while the full record always stays in the Apify dataset.

### Use Cases

- **Job board and aggregator builders:** mirror Jora search results for a set of keywords and locations into your own listings feed.
- **Recruiter and staffing sourcing:** pull matching roles across countries into one dataset to build outreach and candidate-facing lists.
- **Compensation and labor market research:** collect salary ranges, work types, and posting volume across roles and regions.
- **Hiring signal tracking:** schedule a search and watch NEW and REAPPEARED postings to spot a company's hiring activity over time.
- **Job alert tools and bots:** feed newly posted, quick apply, or high salary jobs into your own notification, Slack, or email tool.

### Data You Get

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

| Field | Example |
| --- | --- |
| `id` / `jobId` | `00000000000000000000000000000001` (same value, both fields) |
| `title` | `Sample Software Engineer` |
| `company` | `Sample Company` |
| `location` | `Sydney NSW` |
| `countryCode` | `au` |
| `url` | `https://au.jora.com/job/Sample-Job-00000000000000000000000000000001` |
| `sourceUrl` | `https://au.jora.com/software-engineer-jobs-in-Sydney-NSW` |
| `salary` | `$95,000 - $115,000 a year` |
| `salaryMin` / `salaryMax` | `95000` / `115000` |
| `workType` | `Full time` |
| `workArrangement` | `Hybrid` |
| `postedAtText` | `Posted 3d ago` |
| `quickApply` | `true` |
| `isSponsored` | `false` |
| `rank` / `page` | `1` / `1` |
| `abstract` / `snippetBullets` | `"Short summary from the search card."` / `["Build sample services", "Work with a sample team"]` |
| `descriptionText` / `descriptionHtml` | Full description in plain text and HTML (detail mode only) |
| `applyUrl` | `https://au.jora.com/job/rd/00000000000000000000000000000001` (detail mode only) |
| `sourceName` | `Sample Source` (detail mode only) |
| `searchKeywords` / `searchLocation` / `searchTotalCount` | `"software engineer"` / `"Sydney, NSW"` / `250` |
| `detailFetched` | `true` |
| `job_id`, `job_type`, `date_posted`, `description_text`, `description_html`, `source`, `sponsored`, `country`, `search_keyword`, `search_location`, `search_page` | Compatibility aliases: the same values as `jobId`, `workType`, `postedAtText`, `descriptionText`, `descriptionHtml`, `sourceName`, `isSponsored`, `countryCode`, `searchKeywords`, `searchLocation`, `page`, so pipelines written for other Jora scrapers keep working |
| `scrapedAt` | `2026-01-01T00:00:00.000Z` |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | `"NEW"` / `["salary"]` / `2026-01-01T00:00:00.000Z` / `2026-01-02T00:00:00.000Z` (incremental mode only) |

### How to Use

1. Pick a **mode**: `search` (keywords, location, and filters) or `url` (paste Jora search or job links).
2. Fill in one or more searches, or paste your URLs, and switch on **Fetch full job details** if you need descriptions and apply links.
3. Set **Max items** (and **Max pages** if you want a bounded crawl) to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV, or Excel, or read it through the API.

**Search by keyword and location:**

```json
{
  "mode": "search",
  "searches": [
    {
      "keywords": "software engineer",
      "location": "Sydney, NSW",
      "country": "au"
    }
  ],
  "maxItems": 20
}
```

**Search with filters:**

```json
{
  "mode": "search",
  "searches": [
    {
      "keywords": "developer",
      "location": "Melbourne VIC",
      "country": "au"
    }
  ],
  "postedWithin": "7d",
  "workType": "contract",
  "quickApply": true,
  "sort": "date",
  "salaryMin": 90000,
  "maxItems": 25,
  "fetchDetails": true
}
```

**Run several searches in one run:**

```json
{
  "mode": "search",
  "searches": [
    { "keywords": "data analyst", "location": "Auckland", "country": "nz" },
    { "keywords": "data analyst", "location": "Singapore", "country": "sg" }
  ],
  "maxItems": 40
}
```

**Start from URLs:**

Supported URL shapes (the default and the form prefill carry one example of each search shape):

| Shape | Example |
| --- | --- |
| Keyword and location path | `https://au.jora.com/software-engineer-jobs-in-Sydney-NSW` |
| Same path, page 2 (the page in the link is honoured) | `https://au.jora.com/Software-Engineer-jobs-in-Sydney-NSW?p=2` |
| Keyword-only path | `https://au.jora.com/Software-Engineer-jobs` |
| Location-only path | `https://au.jora.com/jobs-in-Sydney-NSW` |
| Search path, newest first | `https://au.jora.com/Developer-jobs-in-Sydney-NSW?st=date` |
| Search path, salary, quick apply, radius filters | `https://au.jora.com/Developer-jobs-in-Sydney-NSW?sa=90000&qa=y&r=25` |
| Job detail page | `https://au.jora.com/job/Software-Engineer-0155ab88203c8731b751b7006d496e7f` |
| `/j` query search (pasted links only) and job detail links | `https://au.jora.com/j?q=nurse&l=Melbourne`, `https://au.jora.com/job/Sample-Job-00000000000000000000000000000001` |

Search mode builds the keyword and location path form (`/<keyword>-jobs-in-<Location>`) with your filters added as link parameters, because Jora's `/j?q=&l=` form is usually challenged on residential connections. Only keyword or location text that cannot be written in a path (for example symbols such as `+`) falls back to the `/j` form, and the run logs that it usually needs the backup connection.

Behaviour change: running URL mode with `urls` left out now scrapes these seven examples (up to your item limit) instead of returning nothing. Jora may ignore `q` and `l` on the `/jobs` form and serve its general list; the run logs a warning for each such link and leaves `searchKeywords` and `searchLocation` empty for it. Links that are not a recognised Jora search or job address also get a warning.

**URL mode input:**

```json
{
  "mode": "url",
  "urls": [
    "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
    "https://au.jora.com/job/Sample-Job-00000000000000000000000000000001"
  ],
  "maxItems": 20
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/jora-jobs-scraper").call(run_input={
    "mode": "search",
    "searches": [{"keywords": "software engineer", "location": "Sydney, NSW", "country": "au"}],
    "maxItems": 20,
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["company"], job["salary"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/jora-jobs-scraper').call({
    mode: 'search',
    searches: [{ keywords: 'software engineer', location: 'Sydney, NSW', country: 'au' }],
    maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets, or webhooks from the **Integrations** tab.

#### Caps across one or more searches

**Max items** caps the whole run's saved rows. **Max pages** caps how many result pages each search walks; leave it empty to walk every page for that search until Jora's own end of results, with Max items still the main stop. When a run has more than one search, Max items is split evenly across them up front. A search that matches fewer jobs than its even share does not hand the leftover budget to another search in the same run, so a run with several searches can return fewer rows than Max items even though another search in it still has more matching jobs. For an exhaustive multi search run, set Max items generously (or `0` for unlimited) or run each search on its own.

Within a single result page, every job the page returns is marked as already handled for this run before the jobs beyond a search's share are cut from what gets saved. With one search this changes nothing (those jobs are past Max items anyway). With several searches, a job cut from an earlier search's share is skipped by every later search in the same run too, even when a later search also matches it and still has budget, so that job is missing from this run's output. It is not reported as expired (it was seen on the site), and it is picked up again on a later run that reaches it inside the cap.

#### Resume, recurring updates, and incremental mode

Resuming and incremental monitoring are two different things. Pick the one that matches what you are doing:

| Need | Use |
| --- | --- |
| One long pull stopped and should continue | `resumeFromRunId` / the automatic checkpoint |
| Run the same search every day and receive only what changed | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume from a previous run** (`resumeFromRunId`) continues one interrupted run: paste the ID of a previous run (or its dataset), and this run skips every `jobId` already saved there, appending only new jobs. A **checkpoint** is also saved automatically as the run progresses. If a platform migration interrupts the run, or you click **Resurrect** on a failed run, it picks back up in the *same* run without re-saving (or re-charging for) jobs it already collected. No input is needed for this; it is automatic. Two limits apply to both kinds of resume, see the edge cases below: the result page that was being processed at the moment of the interruption is not fully recovered, and a resumed walk can stop early if Jora's listing shifted in between.

**Incremental mode** (`incrementalMode`) is for a schedule. The actor remembers the previous run of the *same* search by itself, so there is no run ID to paste. The first run returns every matching job as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` jobs; everything unchanged since last time is suppressed and not billed unless `emitUnchanged` is on. `UPDATED` rows carry `changedFields` naming exactly which fields moved, and every row carries `firstSeenAt` and `lastSeenAt`. State is isolated automatically: each distinct search scope (mode, `searches`/`urls`, and the filters) gets its own state, and so does each `fetchDetails` setting. Set `stateKey` to give a campaign a stable name, or to deliberately point two differently configured runs at the same state.

Eight fields are left out of change detection on purpose, because they move on their own between runs while the job itself is untouched: `url` and `applyUrl` (a per-request tracking token), `postedAtText` (a relative age that drifts by itself), `rank`, `page`, `sourceUrl` and `searchTotalCount` (belong to the search position, not the job), and `scrapedAt` (a per-run timestamp). All of them are still returned on every row exactly as before; only change detection ignores them, so a job that only moved to another result page is not reported as changed. `ignoreFieldsForChanges` lets you add your own fields to that list (a dotted path such as `a.b` reaches nested fields); they stay in the output, and the job id can never be ignored. Baselines saved by earlier versions are re-compared with the current ignore list, so updating does not produce a one-time wave of `UPDATED` rows.

A few edge cases are worth knowing about before you rely on incremental mode for billing-sensitive monitoring:

- **Emit expired jobs is not limited by Max items.** If the tracked search has genuinely dropped a large number of postings since the last run, every one of them comes back (and is billed) as `EXPIRED` in that one run, with no cap.
- **Bootstrapping a monitoring campaign from Resume from a previous run** (turning on both `resumeFromRunId` and `incrementalMode` together to seed a brand-new state from an existing dataset) makes the very next scheduled run report every one of those resumed jobs as `UPDATED`, with `changedFields` listing every field, even when nothing about the job changed. This is a one-time artifact of the bootstrap; runs after that classify normally.
- **A run interrupted mid-walk by a platform migration, an abort, or a manual Resurrect** does not re-save the jobs it had already saved when that same run resumes, but three side effects remain:
  - Jobs on the result page that was being processed at the moment of the interruption can be lost from that run: the ones not yet saved are skipped after the resume, and the ones saved after the last checkpoint are not counted, so the run can end a page's worth of rows over Max items.
  - If Jora's listing shifted before the resume (for example, one new posting at the top), the resumed walk can mistake an already collected page for the end of results and stop early. The remaining pages are not collected, and with Incremental mode and Emit expired jobs on, the tracked jobs on them are reported (and billed) as `EXPIRED` even though they are still live.
  - With Incremental mode on, the state saved for the *next* scheduled run does not include the jobs saved before the interruption, so they come back once more as `NEW`, `UPDATED`, or `REAPPEARED` (and are billed again) on the following scheduled run.
- **Resume from a previous run has the same early stop.** If new postings appeared since the run you resume from, the first page can hold a fresh job followed by a page of jobs you already have, and the walk ends there, returning only a few new jobs. Resume soon after the original run, or run a normal snapshot instead when the search moves quickly.
- **Two searches in the same run that return overlapping postings** can end one search's walk earlier than its real last page: a page made entirely of jobs already claimed by an earlier search looks like the end of results. The later pages of that search are not collected, and with Emit expired jobs on, tracked jobs that only appear on those pages are reported (and billed) as `EXPIRED` although they are still live. If you rely on Emit expired jobs, run overlapping searches as separate runs, each with its own state, rather than combining them in one multi-search input.
- **A manual State key is not tied to the search.** When you set `stateKey`, the actor does not check whether the search, filters, or `fetchDetails` changed. Changing the search under the same key reports every job of the old search as `EXPIRED` (with Emit expired jobs on) and every job of the new one as `NEW`; switching `fetchDetails` on reports every job as `UPDATED`. Use a new State key when you change what the campaign tracks.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped jobs 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 for each selected job: title, company, location, salary, work type, posted text, and links. 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.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | Choose `search` for keyword/filter searches or `url` for existing Jora URLs. |
| `searches` | array | software engineer in Sydney | Search mode inputs. Each item can include `keywords`, `location`, and `country`. |
| `postedWithin` | string | `anytime` | Date filter: `anytime`, `24h`, `7d`, `14d`, or `30d`. |
| `workType` | string | `any` | Job type filter such as `full_time`, `part_time`, `contract`, `permanent`, or `internship`. |
| `quickApply` | boolean | `false` | Limits results to Jora quick apply jobs. |
| `sort` | string | `relevance` | Use `relevance` or `date`. |
| `distanceKm` | integer | empty | Optional location radius. |
| `salaryMin` | integer | empty | Optional minimum salary filter. |
| `urls` | array | seven example URLs | URL mode inputs. Accepts search result URLs and job detail URLs. If you leave `urls` out in URL mode, the seven examples under Supported URL shapes are scraped instead of nothing. |
| `fetchDetails` | boolean | `true` | Opens job pages for full details. |
| `maxItems` | integer | `20` | Maximum matching rows to save. `0` means unlimited. Results can be fewer when the source has fewer matching jobs. |
| `maxPages` | integer | empty | Maximum search pages per source. Leave empty to walk every result page for that search until Jora has no next page; `maxItems` remains the main cap. |
| `resumeFromRunId` | string | empty | A previous run ID or dataset ID to continue a full catalogue walk across separate runs. Jobs already collected there (matched by `jobId`) are skipped, so this run only saves the new ones. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of this same search. The first run returns everything as `NEW`; later runs return only `NEW`, `UPDATED`, and `REAPPEARED` rows by default. See "Resume, recurring updates, and incremental mode" above. |
| `stateKey` | string | empty | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Derived automatically from your search scope and `fetchDetails` setting when left empty. |
| `ignoreFieldsForChanges` | string\[] | `[]` | Incremental mode only: fields whose changes do not make a job `UPDATED` (they stay in the output). |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return jobs unchanged since the last run, marked `UNCHANGED`. Adds, and bills for, extra rows you already have. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return jobs from a previous run that are no longer found, marked `EXPIRED`, once a run has covered the whole tracked search (not capped by `maxItems`/`maxPages`, not a resume). Adds, and bills for, extra rows, with no cap of its own. |
| `proxy` | object | Residential AU | Connection settings. Residential AU is selected by default. |
| `mcpConnectors` | array | empty | Optional MCP connector ids for app export. |
| `notionParentPageUrl` | string | empty | Required only for Notion page export. |
| `maxNotifyListings` | integer | `50` | Maximum jobs sent to each connector. Does not affect the dataset. |
| `startUrl` | string | empty | Compatibility alias for `urls`: one Jora link, which switches the run to URL mode. A one-item list is accepted too. Applies whenever `urls` is left at its prefilled examples or empty; a `urls` list you changed wins. |
| `keyword` | string | empty | Compatibility alias for the keywords of `searches`. Applies only when `searches` is not set. |
| `location` | string | empty | Compatibility alias for the location of `searches`. Applies only when `searches` is not set. |
| `country` | string | empty | Compatibility alias for the country of `searches`; accepts a code such as `au` or a label such as `Australia`. |
| `posted_date` | string | empty | Compatibility alias for `postedWithin`. Applies while Posted within is `anytime`. Accepts `24h`, `7d`, `14d`, `30d`, `anytime` and plain forms such as `today`, `week`, `month`; any other value is ignored and the run logs a warning. |
| `results_wanted` | integer | empty | Compatibility alias for `maxItems`. Applies while Max items is still `20`; a Max items you changed wins. |
| `max_pages` | integer | empty | Compatibility alias for `maxPages`. Applies when Max pages is empty. |
| `proxyConfiguration` | object | empty | Compatibility alias for `proxy`. Applies while Proxy is still the Residential AU default; a Proxy you changed wins. |

### Output Example

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

```json
{
  "id": "00000000000000000000000000000001",
  "jobId": "00000000000000000000000000000001",
  "title": "Sample Software Engineer",
  "company": "Sample Company",
  "location": "Sydney NSW",
  "countryCode": "au",
  "url": "https://au.jora.com/job/Sample-Job-00000000000000000000000000000001",
  "sourceUrl": "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
  "salary": "$95,000 - $115,000 a year",
  "salaryMin": 95000,
  "salaryMax": 115000,
  "workType": "Full time",
  "workArrangement": "Hybrid",
  "postedAtText": "Posted 3d ago",
  "quickApply": true,
  "isSponsored": false,
  "rank": 1,
  "page": 1,
  "abstract": "Short summary from the search card.",
  "snippetBullets": ["Build sample services", "Work with a sample team"],
  "descriptionHtml": "<div>Full job description appears here.</div>",
  "descriptionText": "Full job description appears here when fetchDetails=true.",
  "applyUrl": "https://au.jora.com/job/rd/00000000000000000000000000000001",
  "sourceName": "Sample Source",
  "searchKeywords": "software engineer",
  "searchLocation": "Sydney, NSW",
  "searchTotalCount": 250,
  "detailFetched": true,
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

#### Output views

The dataset has four views: Overview (the key columns), Full (every field), Detail (description, apply link and source, for detail runs) and Changes (change type, changed fields and first and last seen, for incremental runs). Each is also available as an output tab of the run.

### Plan Requirement

Jora protects its search and job detail pages with a managed browser challenge, so the connection matters. Residential AU is selected by default, and plain datacenter connections are not enough on their own: this is not a "works with no action needed" actor. A paid Apify plan is recommended, because Apify free plans do not include residential proxies. For searches outside Australia, choose the closest Residential country supported by your Apify plan, or supply your own proxy URLs.

What you see when the connection is not available (exact log and failure texts):

- Free plan warning in the log, then the run continues on a backup connection: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs."
- When the backup connection serves pages, the log says: "Served over the backup connection (the default connection was unavailable)." The backup connection is used only when the default one is challenged or refused, and its traffic is billed to your own Apify account. Pages served over it are slower (up to about a minute each), and the run allows for that.
- Run fails, proxy refused for the account: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
- Search text that cannot be written as a search path: "This search text cannot be written as a Jora search path, so it is requested with the query form, which usually needs the backup connection."
- Run fails, free plan and the backup could not load pages: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Jora pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- Run fails, any other connection problem: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

#### Billing

The run charges three events: the actor start event (once per run, per GB of memory, minimum one), the dataset item event (once per job saved to the dataset), and the `detail-enrichment` event (once per job saved with full details, only when Fetch full job details is on). Suppressed unchanged jobs and failed pages are not charged. The Pricing tab shows the current rates.

### FAQ

#### How much does it cost?

You pay per job saved to the dataset, plus the `detail-enrichment` event for each job scraped with full details on, and the actor start event once per run (see Billing). The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of a run; note that `emitExpired` rows are not covered by that cap (see "Resume, recurring updates, and incremental mode" above).

#### Is it legal to scrape Jora?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow Jora's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution of the results.

#### Can I get only new or changed jobs on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated, and reappeared jobs, and unchanged ones are not billed by default.

#### What is the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run so you do not re-save the same jobs twice; you paste an ID. **Incremental mode** is for running the identical search again and again (for example, daily) and getting only what changed; the actor tracks its own state and there is no ID to paste. The two are normally not combined, except to seed a brand-new monitoring campaign from an existing full pull (see the tip on that in "Resume, recurring updates, and incremental mode").

#### Why did a run return fewer jobs than Max items when I used multiple searches?

Max items is split evenly across the searches in that run up front. A search that matches fewer jobs than its share does not pass its unused budget to another search in the same run. Raise Max items (or set it to `0`), or run each search separately, if you need every search to return its full share.

#### Why did my run fail or return few jobs?

If no results page could be loaded at all, the run fails on purpose with one of the messages listed under Plan Requirement, instead of finishing as an empty success. If some pages loaded and a later page stays empty after several retries on fresh connections, that search stops with a warning and the run finishes with what it collected, so check the log for "empty response" warnings and run it again. With Incremental mode on, a search stopped this way never triggers `EXPIRED` rows. A run also fails on purpose when `resumeFromRunId` does not point to a run or dataset your account can read, or when `resumeFromRunId` is combined with Incremental mode for a search that already has saved state.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear, or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/cwjobs-scraper"><b>CWJobs UK Scraper</b></a><br>Scrape CWJobs.co.uk tech and IT job listings by keyword, location, salary, work type...</td><td>💼 <a href="https://apify.com/abotapi/jobstreet-scraper"><b>JobStreet Scraper</b></a><br>Scrape JobStreet listings across Malaysia, Singapore, Indonesia, and the Philippines...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/we-work-remotely-jobs-scraper"><b>WeWorkRemotely Scraper</b></a><br>Extract current remote job listings from weworkremotely.com. Use keyword search or WWR...</td><td>💼 <a href="https://apify.com/abotapi/remoteok-com-scraper"><b>RemoteOK Jobs Scraper</b></a><br>Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/simplyhired-com-scraper"><b>SimplyHired Scraper</b></a><br>Scrape active SimplyHired jobs across the US, UK, Canada, Australia, Ireland and India...</td><td>💼 <a href="https://apify.com/abotapi/seek-scraper"><b>SEEK Jobs Scraper</b></a><br>Scrape SEEK.com.au and SEEK.co.nz jobs by keyword, location, or filters. Extract full...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/jora-jobs-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Choose search mode to build Jora searches from filters, or URL mode to start from existing Jora search/detail URLs.

## `searches` (type: `array`):

One or more keyword/location searches. Each search can target a different country.

## `postedWithin` (type: `string`):

Only return jobs listed within this time window.

## `workType` (type: `string`):

Filter by Jora work type.

## `quickApply` (type: `boolean`):

Return only jobs marked as quick apply by Jora.

## `sort` (type: `string`):

Sort search results by Jora relevance or newest listed.

## `distanceKm` (type: `integer`):

Optional radius around the location. Leave empty to use Jora's default.

## `salaryMin` (type: `integer`):

Optional minimum salary filter.

## `urls` (type: `array`):

Multi-URL supported: keyword/location search paths, /jobs?q=\&l= and /j?q=\&l= searches, and job detail links. Search filters are ignored. Leave the field out and these examples are scraped. Search result URLs honor the page number in the pasted URL. Detail URLs save that job only.

## `fetchDetails` (type: `boolean`):

When enabled, adds full description, apply URL, and detail-only fields. Detail records use the detail-enrichment charge event.

## `maxItems` (type: `integer`):

Maximum number of matching jobs to save. A run can return fewer rows when the selected search, URL, and filters match fewer current jobs. Use 0 for unlimited.

## `maxPages` (type: `integer`):

Maximum search result pages per search URL. Leave empty to walk every result page; maxItems remains the main cap.

## `proxy` (type: `object`):

Residential proxy is selected by default because Jora can return empty responses from cloud datacenter connections.

## `proxyConfiguration` (type: `object`):

Same as Proxy; used while Proxy is still the Residential AU default.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Jobs already collected there (matched by jobId) are skipped, so this run only saves the new ones. Use this after an interrupted run, or to keep growing one big catalogue pull. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead — the two are different things and normally not combined.

## `incrementalMode` (type: `boolean`):

Turn this on for daily/weekly monitoring of the same search. The first run returns every matching job as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs — unchanged ones are skipped (and not billed). State is kept separately per search scope (mode, searches/URLs, and filters); use State key below to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search scope (searches/URLs and filters) — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max items/Max pages capped it, the site blocked a page, or Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Incremental mode only. Field names (for example abstract or salary; a dotted path like a.b reaches nested fields) whose changes should NOT make a job UPDATED. The fields stay in the output and are never removed. Page position, rank, the search total, the scrape time, the tracking links and the relative posting age are always ignored. The job id can never be ignored. Leave empty to compare everything else.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify Settings API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## `startUrl` (type: `string`):

A single Jora URL. Same as putting one link in Jora URLs; switches to URL mode when Mode is not set.

## `keyword` (type: `string`):

Job keyword. Builds one search when Searches is empty.

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

Location for the keyword search, for example Sydney, NSW.

## `country` (type: `string`):

Jora country site for the keyword search: a code (au, nz, sg, hk, my, id, us, uk, ca) or a label such as Australia. Defaults to au.

## `posted_date` (type: `string`):

anytime, 24h, 7d, 14d or 30d (plain forms such as today, week or month also work). Used while Posted within is anytime.

## `results_wanted` (type: `integer`):

Same as Max items; used while Max items is still 20.

## `max_pages` (type: `integer`):

Same as Max pages; used when Max pages is not set.

## Actor input object example

```json
{
  "mode": "search",
  "searches": [
    {
      "keywords": "software engineer",
      "location": "Sydney, NSW",
      "country": "au"
    }
  ],
  "postedWithin": "anytime",
  "workType": "any",
  "quickApply": false,
  "sort": "relevance",
  "urls": [
    "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
    "https://au.jora.com/Software-Engineer-jobs-in-Sydney-NSW?p=2",
    "https://au.jora.com/Software-Engineer-jobs",
    "https://au.jora.com/jobs-in-Sydney-NSW",
    "https://au.jora.com/Developer-jobs-in-Sydney-NSW?st=date",
    "https://au.jora.com/Developer-jobs-in-Sydney-NSW?sa=90000&qa=y&r=25",
    "https://au.jora.com/job/Software-Engineer-0155ab88203c8731b751b7006d496e7f"
  ],
  "fetchDetails": true,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AU"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `full` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "searches": [
        {
            "keywords": "software engineer",
            "location": "Sydney, NSW",
            "country": "au"
        }
    ],
    "urls": [
        "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
        "https://au.jora.com/Software-Engineer-jobs-in-Sydney-NSW?p=2",
        "https://au.jora.com/Software-Engineer-jobs",
        "https://au.jora.com/jobs-in-Sydney-NSW",
        "https://au.jora.com/Developer-jobs-in-Sydney-NSW?st=date",
        "https://au.jora.com/Developer-jobs-in-Sydney-NSW?sa=90000&qa=y&r=25",
        "https://au.jora.com/job/Software-Engineer-0155ab88203c8731b751b7006d496e7f"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "AU"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/jora-jobs-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 = {
    "mode": "search",
    "searches": [{
            "keywords": "software engineer",
            "location": "Sydney, NSW",
            "country": "au",
        }],
    "urls": [
        "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
        "https://au.jora.com/Software-Engineer-jobs-in-Sydney-NSW?p=2",
        "https://au.jora.com/Software-Engineer-jobs",
        "https://au.jora.com/jobs-in-Sydney-NSW",
        "https://au.jora.com/Developer-jobs-in-Sydney-NSW?st=date",
        "https://au.jora.com/Developer-jobs-in-Sydney-NSW?sa=90000&qa=y&r=25",
        "https://au.jora.com/job/Software-Engineer-0155ab88203c8731b751b7006d496e7f",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "AU",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/jora-jobs-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 '{
  "mode": "search",
  "searches": [
    {
      "keywords": "software engineer",
      "location": "Sydney, NSW",
      "country": "au"
    }
  ],
  "urls": [
    "https://au.jora.com/software-engineer-jobs-in-Sydney-NSW",
    "https://au.jora.com/Software-Engineer-jobs-in-Sydney-NSW?p=2",
    "https://au.jora.com/Software-Engineer-jobs",
    "https://au.jora.com/jobs-in-Sydney-NSW",
    "https://au.jora.com/Developer-jobs-in-Sydney-NSW?st=date",
    "https://au.jora.com/Developer-jobs-in-Sydney-NSW?sa=90000&qa=y&r=25",
    "https://au.jora.com/job/Software-Engineer-0155ab88203c8731b751b7006d496e7f"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AU"
  }
}' |
apify call abotapi/jora-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/jora-jobs-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/zhacco2It2Esxn1vb/builds/vmXuslVOvq5gnIXho/openapi.json
