# CareerJunction Jobs Scraper: South Africa Job Listings (`hexagone-data/careerjunction-scraper`) Actor

Scrape careerjunction.co.za job searches and categories: job title, company, location, salary (text + ZAR min/max), job type, seniority, posting and expiry dates, optional full description. Pay per result.

- **URL**: https://apify.com/hexagone-data/careerjunction-scraper.md
- **Developed by:** [Hexagone Data](https://apify.com/hexagone-data) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 jobs

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?

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

## CareerJunction Jobs Scraper — South African Job Postings

Collect job postings from **careerjunction.co.za**, a South African job board. Paste search results URLs with your own keywords and filters, category or location pages, or single job URLs, and get one record per job: title, company, location, salary as displayed with ZAR amounts, contract type, seniority and posting date.

Use dated exports to track a market, match postings by `jobId`, or monitor a sector over time. This Actor reads the public result pages.

### Try it with 20 jobs

1. Paste a search results URL into **Start URLs**.
2. Set **Maximum number of jobs** to `20`.
3. Start the Actor and open its dataset.
4. Read **Run summary** before exporting JSON, CSV or Excel: a successful run can still contain incomplete URLs or a listing the site cut short.

```json
{
  "startUrls": [{"url": "https://www.careerjunction.co.za/jobs/results?keywords=engineer"}],
  "maxItems": 20
}
```

The form's job-count prefill is 20; the schema default is `0` (no limit). A broad keyword like *engineer* can span many pages; the number of jobs changes over time — set `maxItems` explicitly in API calls to bound the run.

### Choose your start URLs

| Input | Data | Notes |
|---|---|---|
| Search `/jobs/results?keywords=…` | Jobs matching the search, with the filters exactly as you chose them on the site (location, salary, contract type...) | Keep the URL the site gave you; the Actor collects from page 1 and follows pagination to the last page announced. |
| Category or location `/jobs/<name>` (e.g. `/jobs/it`, `/jobs/cape-town`) | Jobs of that category or city | Same listing format, same pagination. |
| Single job `/<slug>-job-<id>.aspx` | One job record, built from the job page's structured data | A job collected this way wins the deduplication against its listing-card duplicate. |

You can mix all three. Jobs are deduplicated within the run by `jobId`. An invalid or disallowed URL is reported and skipped; it never blocks the others.

### Full details (includeDetails)

For listing URLs, the Actor reads result pages by default, usually 25 jobs per page; fields absent from the page remain null. Enable **Open each job page for full details** to additionally open each delivered job's page once, for:

- `description`: the complete job description text,
- exact `salaryMin` / `salaryMax` from the site's structured data (it can be more precise than the displayed text),
- `category`: the published sector, e.g. *agriculture, fishing & forestry*,
- `expiresAt`: the exact expiry datetime, and the exact `postedAt` datetime.

This makes the run slower — one extra request per delivered job — and the Actor only does it for jobs it actually delivers (within `maxItems`). A job page that cannot be opened does not stop the run: that job is delivered without description and the run summary says how many pages failed.

### Output

| Field | Meaning |
|---|---|
| `jobId` | CareerJunction job identifier (deduplication key). |
| `url`, `title` | Job posting URL and displayed title. |
| `company`, `companyUrl`, `companyLogo` | Published employer or recruiting company, its profile URL and logo (logo links are signed by the site and can expire). Company names only — no personal contact details are extracted. |
| `location` | Location as displayed, e.g. *Johannesburg*. |
| `salary`, `salaryMin`, `salaryMax`, `salaryPeriod` | Salary exactly as displayed (*R35,000 - R41,700 per month*, *R Undisclosed*), numeric bounds in ZAR when the text writes amounts, and the pay period. Undisclosed salaries keep their text with null numbers. Some recruiters mask a salary as *R01 - R02 per month*: the numbers are delivered as displayed, the text tells you what the site actually showed. |
| `currency` | Set to `ZAR` by this Actor, which is designed for salaries displayed in South African rand; no currency conversion is performed. |
| `postedAt` | Posting date, ISO (`2026-10-20` from the card; exact datetime with includeDetails). |
| `jobType`, `seniority` | From the displayed position line: *Permanent* / *Contract* / *Temporary*, and *Junior* / *Senior* / *Specialist*... |
| `category`, `description`, `expiresAt` | Job-page details; only filled with includeDetails or for a single job URL. Description text is delivered as published, including any contact text or masks supplied by the site; this is not an anonymisation service. |
| `expiresIn` | Expiry as displayed on the card (*Expires in 5 days*). |
| `page`, `position`, `sourceUrl`, `scrapedAt` | Provenance: listing page and card position, start URL, UTC read time. |

An absent field is `null`, never an empty string.

#### Real output example

This exact listing record comes from run `zJZp8JEJeEOtbXqaH` (build `0.1.6`), completed on **2026-10-22T22:12:15.656Z**. It demonstrates the format, not today's postings. Signed logo URLs can expire.

```json
{
  "jobId": "2646461",
  "url": "https://www.careerjunction.co.za/agriculture-engineer-uae-job-2646461.aspx",
  "title": "Agriculture Engineer – UAE",
  "company": "Hire Resolve",
  "companyUrl": "https://www.careerjunction.co.za/companies/24781/hire-resolve",
  "companyLogo": "https://careerjunction-web-prd.s3.eu-west-1.amazonaws.com/assets/employer-logos/24781-240x120.png?X-Amz-Expires=86400&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA3UBW7IFPPL4T7PMH/20261022/eu-west-1/s3/aws4_request&X-Amz-Date=20261022T221204Z&X-Amz-SignedHeaders=host&X-Amz-Signature=5a59508e91f5ae6cdfdafc47c1a2b35ca44dfea7bcd59a9a4a632f5e83d73ad0",
  "location": "United Arab Emirates",
  "salary": "R75,000 - R91,700 per month",
  "salaryMin": 75000,
  "salaryMax": 91700,
  "salaryPeriod": "per month",
  "currency": "ZAR",
  "postedAt": "2026-10-21",
  "jobType": "Permanent",
  "seniority": "Senior",
  "category": null,
  "description": null,
  "expiresIn": "Expires in 33 days",
  "expiresAt": null,
  "page": 1,
  "position": 17,
  "sourceUrl": "https://www.careerjunction.co.za/jobs/results?keywords=engineer",
  "scrapedAt": "2026-10-22T22:12:05+00:00"
}
```

With `includeDetails` enabled, the same job carries `"category": "agriculture, fishing & forestry"`, `"expiresAt": "2026-11-24T07:12:22"`, the exact posting datetime and the full description text.

### Run summary

The Actor leaves two readable records:

- **OUTPUT** (JSON): `jobs` delivered, per-URL statuses (`done`, `no_jobs`, `stopped_at_limit`, `not_found`, `partial_collection`...), rejected URLs with reasons, and automatic check warnings (`anomalies`, empty when everything is normal).
- **Run summary** (HTML): the same information as a table, in the console's Output tab.

A listing the site cut short — fewer jobs than the site announced — is reported as `partial_collection` with a check warning, never as a quiet success. A search with no results is `no_jobs`, not a failure. If no job is delivered and at least one accepted URL fails, is interrupted or cannot be parsed, the run fails with an explicit status message. A changed page structure is reported as `structure_changed`, or `partial_collection` if that listing already delivered jobs.

### Troubleshooting and support

Check **Run summary** first. For `partial_collection`, compare the delivered and announced
counts and inspect the affected start URL. For `blocked_by_site`, `rate_limited` or
`structure_changed`, keep the run ID and the rejected/affected URLs when reporting the issue.
A legitimate empty search is reported as `no_jobs` and has no result charge.

Report reproducible problems in the [Issues tab](https://console.apify.com/actors/ibu7MYbm1RaG7nkHC/issues).
Include the run ID, input URLs and expected versus observed result; do not include API tokens
or private credentials. Hexagone Data maintains this Actor. Repeated site changes or failures
require investigation; a successful run with warnings does not establish full coverage.

### Cost and operating limits

- **Price**: charged per delivered job ($1.50 per 1,000 results), never per request. For example, 20 delivered jobs cost $0.03 and 1,000 cost $1.50 in result charges; a run delivering no jobs has no result charge. Any additional platform or proxy costs depend on your Apify configuration. `maxItems` and your charge limit bound the run.
- **Politeness**: about one request every 2 seconds, starting with a plain HTTP client and escalating access if necessary. The Actor rejects `/myprofile/` account URLs based on the robots.txt rules checked on 22 October 2026. Those rules are not refreshed during each run.
- **Durability**: if the platform migrates the run mid-collection, it resumes from its checkpoint without delivering duplicates.

# Actor input Schema

## `startUrls` (type: `array`):

careerjunction.co.za pages to scrape. Accepted: search results (/jobs/results?keywords=... with the filters you chose on the site), category and location pages (/jobs/it, /jobs/cape-town) and single job pages (...-job-<id>.aspx). Pagination is followed to the last page announced by the site.

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

Stops after this many unique jobs across all start URLs. 0 = no limit.

## `includeDetails` (type: `boolean`):

Off by default: listings carry title, company, location, salary text, contract type and posting date. When enabled, each job page is opened once for its full description, exact salary range and sector, which makes the run slower and opens one request per delivered job.

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

Not needed by default. Only enable it if your runs get blocked.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.careerjunction.co.za/jobs/results?keywords=engineer"
    }
  ],
  "maxItems": 20,
  "includeDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

All job records, one per job posting, with every field.

## `jobsOverview` (type: `string`):

Job overview: title, company, location, salary, contract type, posting date.

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

Readable per-URL summary: status, jobs delivered, reason for any URL that returned nothing.

## `summaryJson` (type: `string`):

The same summary as JSON (OUTPUT record), for programmatic use.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.careerjunction.co.za/jobs/results?keywords=engineer"
        }
    ],
    "maxItems": 20,
    "includeDetails": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("hexagone-data/careerjunction-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 = {
    "startUrls": [{ "url": "https://www.careerjunction.co.za/jobs/results?keywords=engineer" }],
    "maxItems": 20,
    "includeDetails": False,
}

# Run the Actor and wait for it to finish
run = client.actor("hexagone-data/careerjunction-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 '{
  "startUrls": [
    {
      "url": "https://www.careerjunction.co.za/jobs/results?keywords=engineer"
    }
  ],
  "maxItems": 20,
  "includeDetails": false
}' |
apify call hexagone-data/careerjunction-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hexagone-data/careerjunction-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/ibu7MYbm1RaG7nkHC/builds/F7jlPVnC2ZBajXBz6/openapi.json
