# Middle East Jobs Scraper — 5 Boards, Deduplicated (`almoutasem_nabil/mena-jobs-aggregator`) Actor

Job postings from Bayt, GulfTalent, NaukriGulf, Wuzzuf and Akhtaboot in one normalized schema: title, company, location, salary, seniority, function. Cross-board duplicates merged. Optional: only jobs posted since your last run.

- **URL**: https://apify.com/almoutasem\_nabil/mena-jobs-aggregator.md
- **Developed by:** [Al Moutasem Nabil](https://apify.com/almoutasem_nabil) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job records

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

## Middle East Jobs Scraper — Bayt, GulfTalent, NaukriGulf, Wuzzuf & Akhtaboot (Deduplicated)

Job postings from the top MENA job boards in one normalized schema: title, company, location,
salary, seniority, function. Cross-board duplicates merged. Optional: only jobs posted since your
last run.

وظائف الخليج والأردن ومصر من خمس منصات في مكان واحد — بيت.كوم، جلف تالنت، نوكري جلف، وظّفني وأخطبوط.
نتائج موحّدة، بدون تكرار، مع تصنيف الراتب والمستوى الوظيفي. السعودية، الإمارات، قطر، الكويت، البحرين،
عُمان، الأردن، مصر، لبنان.

### Who it's for

- **Gulf recruiters and agencies** watching what competitors are hiring for, without opening five tabs.
- **HR-tech products** that need a clean, normalized job feed instead of five different schemas.
- **Salary-report and market analysts** who need comparable pay data across countries and currencies.
- **Job aggregators** that want one deduplicated feed rather than five overlapping ones.

### What makes it different

Single-board scrapers already exist. The layer on top does not:

- **Five boards, one schema.** Every posting comes back with the same fields whichever board it came from.
- **Cross-board deduplication.** The same vacancy posted to Bayt, GulfTalent and NaukriGulf returns
  **once**, with all three URLs under `boards`. Matching is on normalized company + title + country
  - city, plus a Jaro-Winkler similarity check that catches "Senior iOS Developer",
    "Senior iOS Developer (m/f)" and "senior ios developer - urgent" as one job.
- **Arabic-aware.** Titles and companies are normalized across alef forms, taa marbuta, yaa and
  tashkeel, so Arabic postings deduplicate as reliably as English ones. Arabic-Indic digits are read
  in salaries.
- **Normalized salary.** All eight MENA currencies plus USD, monthly vs yearly detected from the
  text or inferred from magnitude, with the original string always kept in `salary.raw`.
- **New since last run.** Change mode emits only `job_added`, `job_removed` and `job_updated`.

### Output

One item per unique vacancy. Every field is documented in `.actor/dataset_schema.json`; the Console
shows **Jobs** and **New jobs** views. A merged cross-posting looks like this:

```json
{
    "jobKey": "acme|senior-ios-developer|SA|riyadh",
    "title": "Senior iOS Developer",
    "titleNormalized": "senior ios developer",
    "company": "Acme Trading LLC",
    "companyNormalized": "acme",
    "location": { "raw": "Riyadh - Saudi Arabia", "city": "riyadh", "country": "SA" },
    "remote": "onsite",
    "function": "engineering",
    "seniority": "senior",
    "employmentType": "full_time",
    "salary": {
        "min": 15000,
        "max": 20000,
        "currency": "SAR",
        "period": "month",
        "raw": "SAR 15,000 - 20,000 per month"
    },
    "postedAt": "2026-09-01T00:00:00.000Z",
    "boards": [
        {
            "board": "bayt",
            "url": "https://www.bayt.com/en/saudi-arabia/jobs/senior-ios-developer-5123456/",
            "postedAt": "2026-09-02T00:00:00.000Z",
            "boardJobId": "5123456"
        },
        {
            "board": "gulftalent",
            "url": "https://www.gulftalent.com/saudi-arabia/jobs/senior-ios-developer-629150",
            "postedAt": null,
            "boardJobId": "629150"
        },
        {
            "board": "naukrigulf",
            "url": "https://www.naukrigulf.com/senior-ios-developer-jobs-...",
            "postedAt": "2026-09-01T00:00:00.000Z",
            "boardJobId": "150726500829"
        }
    ],
    "language": "en",
    "firstSeenAt": "2026-09-01T00:00:00.000Z",
    "lastSeenAt": "2026-09-07T00:00:00.000Z",
    "scrapedAt": "2026-09-07T00:00:00.000Z"
}
```

Boards that could not be read appear as error items with `board` and `error`, and are **never charged**.

### Pricing

Pay-per-event. You pay for boards actually read and jobs actually returned.

| Event           | Price  | When                                                                      |
| --------------- | ------ | ------------------------------------------------------------------------- |
| Board checked   | $0.005 | Per board fetched successfully. A blocked board is free.                  |
| Job record      | $0.002 | Snapshot mode, per **unique** job — a role on three boards is one charge. |
| Change detected | $0.01  | Change mode, per change event.                                            |

- **Weekly pull of all iOS jobs in SA + AE**, ~300 unique jobs across 5 boards:
  5 × $0.005 + 300 × $0.002 = **$0.625 per run**.
- **Daily new-jobs feed**, 5 boards, quiet day: **$0.025**. With 12 new jobs: **$0.145**.

Apify's `apify-actor-start` fee and platform compute are billed separately. Default memory 512 MB.

### Scheduling, integrations and API

1. Fill in the input, **Save as task**, then add a **Schedule** — daily at 07:00 is typical.
2. Turn on **Emit changes only** with a **Tracking ID** so each run returns only what is new.
3. Under **Integrations**, push results to Slack, Google Sheets, a webhook, Make or Zapier on
   *Run succeeded*. In change mode an empty dataset means nothing changed.
4. **Standby mode** answers synchronously, which is what an AI agent wants:
   `GET https://<your-actor>.apify.actor/?keywords=ios%20developer&countries=SA,AE&postedWithinDays=7`

State lives in the named key-value store `mena-jobs-state`, one record per tracking ID; delete it to
reset a watch list.

### Per-board notes and limitations

- **NaukriGulf** — uses the site's own JSON search API. The most reliable of the five, and the only
  one that reliably carries a posting date. Its search payload carries no salary.
- **GulfTalent** — server-rendered country listings. Keyword filtering happens locally, so a narrow
  keyword search fetches more rows than it returns.
- **Akhtaboot** — read from the per-country RSS feeds the site publishes. No salary or city, and no
  keyword parameter, so keywords are filtered locally.
- **Bayt** and **Wuzzuf** — both sit behind Cloudflare and refuse a share of datacenter traffic with
  HTTP 403. The run still succeeds for the other boards and reports these as error items; switch
  `proxyConfiguration` to residential proxies to get them. Wuzzuf is Egypt-only and is skipped
  unless `EG` is among your countries.
- **Salary coverage is board-dependent.** Most MENA postings publish no salary at all; `salary` is
  null rather than guessed.
- Change detection is relative to your filters. Changing `keywords`, `countries` or `boards` between
  runs will look like added and removed jobs.

### Related

Watching specific employers rather than whole boards? **[Hiring Signals Monitor](../hiring-signals-monitor)**
tracks named companies' own career pages through their ATS and reports new roles, removed roles and
hiring surges.

### Legal note

The Actor reads publicly listed job postings — the same pages any visitor sees — and Akhtaboot's own
published RSS feeds. It never logs in and sends no cookies. It collects **business data only**:
company names, titles, locations and salaries. It never collects recruiter names, emails or phone
numbers, and drops such fields where a board's payload includes them. Requests are paced and
concurrency is capped per board. Use the data in line with each board's terms and the laws that
apply to you. Bayt, GulfTalent, NaukriGulf, Wuzzuf and Akhtaboot are trademarks of their owners;
this Actor is not affiliated with them.

# Actor input Schema

## `keywords` (type: `array`):

Job titles or skills, in Arabic or English. A posting matches if any keyword appears in its title or company. Leave empty to take everything the selected boards list for your countries.

## `countries` (type: `array`):

ISO-2 codes: SA, AE, QA, KW, BH, OM, JO, EG, LB. Boards that do not cover a country are skipped for it rather than failing.

## `boards` (type: `array`):

Which boards to search. Bayt and Wuzzuf sit behind Cloudflare and often refuse datacenter proxies - if you see board errors for them, switch proxyConfiguration to residential.

## `postedWithinDays` (type: `integer`):

Ignore postings older than this. 0 keeps everything the boards return.

## `functions` (type: `array`):

Keep only these functions, classified from the title: engineering, data, product, design, sales, marketing, customer\_success, support, operations, finance, hr, legal, other.

## `seniority` (type: `array`):

Keep only these levels: intern, junior, mid, senior, staff, lead, manager, director, vp, c\_level.

## `employmentType` (type: `array`):

Keep only these types: full\_time, part\_time, contract, temporary, internship.

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

Keep only postings that say they are remote.

## `maxJobsPerBoard` (type: `integer`):

Stop after this many postings per board, before filtering and deduplication.

## `dedupe` (type: `boolean`):

Merge the same vacancy across boards into one item listing every board it appeared on. Turn off to get one item per board posting.

## `emitChangesOnly` (type: `boolean`):

Output only what changed since the last run with the same tracking ID: job\_added, job\_removed, job\_updated. Off = output every matching job.

## `trackingId` (type: `string`):

Name of this watch list. Required when 'Emit changes only' is on. Snapshots are stored under this key in the 'mena-jobs-state' key-value store.

## `emitBaselineAsAdded` (type: `boolean`):

On the very first run for a tracking ID, emit every job as job\_added. Off = the first run records the baseline silently.

## `includeDescription` (type: `boolean`):

Include the full job description where the board publishes it. Makes items much larger.

## `maxConcurrency` (type: `integer`):

How many boards are searched in parallel. Lower it if a board starts rate-limiting you.

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

Proxies for the requests. Datacenter is enough for NaukriGulf, GulfTalent and Akhtaboot; Bayt and Wuzzuf usually need residential.

## Actor input object example

```json
{
  "keywords": [
    "iOS developer",
    "accountant"
  ],
  "countries": [
    "SA",
    "AE",
    "JO"
  ],
  "boards": [
    "bayt",
    "gulftalent",
    "naukrigulf",
    "wuzzuf",
    "akhtaboot"
  ],
  "postedWithinDays": 7,
  "remoteOnly": false,
  "maxJobsPerBoard": 500,
  "dedupe": true,
  "emitChangesOnly": false,
  "emitBaselineAsAdded": false,
  "includeDescription": false,
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One item per unique job. A role posted to several boards appears once, with every board listed under 'boards'. In change mode, one item per change event instead.

## `state` (type: `string`):

Snapshot of the jobs seen under each tracking ID, so the next run can report only what changed. One record per tracking ID.

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

Per-board job counts, duplicates merged, salary and country coverage, request counts and the charged-event tally.

# 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 = {
    "keywords": [
        "iOS developer",
        "accountant"
    ],
    "countries": [
        "SA",
        "AE",
        "JO"
    ],
    "boards": [
        "bayt",
        "gulftalent",
        "naukrigulf",
        "wuzzuf",
        "akhtaboot"
    ],
    "postedWithinDays": 7,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("almoutasem_nabil/mena-jobs-aggregator").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 = {
    "keywords": [
        "iOS developer",
        "accountant",
    ],
    "countries": [
        "SA",
        "AE",
        "JO",
    ],
    "boards": [
        "bayt",
        "gulftalent",
        "naukrigulf",
        "wuzzuf",
        "akhtaboot",
    ],
    "postedWithinDays": 7,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("almoutasem_nabil/mena-jobs-aggregator").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 '{
  "keywords": [
    "iOS developer",
    "accountant"
  ],
  "countries": [
    "SA",
    "AE",
    "JO"
  ],
  "boards": [
    "bayt",
    "gulftalent",
    "naukrigulf",
    "wuzzuf",
    "akhtaboot"
  ],
  "postedWithinDays": 7,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call almoutasem_nabil/mena-jobs-aggregator --silent --output-dataset

```

## MCP server setup

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

```

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/QicUjdaiOvUY8YjHY/builds/d7H437IQjbQjqQTkq/openapi.json
