# GulfTalent Scraper - Gulf Jobs, Salaries, Employer & Expiry (`neverempty/gulftalent-jobs`) Actor

Every GulfTalent job across 16 countries: title, employer, city, industry, employment type, posted and expiry dates, and the salary as two numbers with its currency and period when published - 44% of jobs, measured. A country GulfTalent does not know returns a note, not other jobs. Unofficial.

- **URL**: https://apify.com/neverempty/gulftalent-jobs.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.73 / 1,000 job returneds

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?

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

## GulfTalent Scraper - Gulf Jobs, Salaries, Employer & Expiry

Pull jobs from **GulfTalent** across **16 country sites** - UAE, Saudi Arabia, Qatar, Oman, Bahrain, Kuwait, Egypt, Jordan, Lebanon, Iraq, Libya, India, Africa, Far East, Europe and Other - as clean JSON, with the salary as **two numbers plus its currency and period** whenever the employer published one.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by GulfTalent. All product names are trademarks of their respective owners.*

No API key. No proxy needed. Reads the same public pages a visitor sees.

***

### Why this one is different

#### It refuses to hand you the wrong country

This is the trap in GulfTalent, measured on 2026-09-03: **a country it does not recognise answers `HTTP 200`**, not an error - and returns a mixed list of jobs from *other* countries. Asking for `/zzzznot/jobs/title/accountant` came back with 25 jobs: 22 from the UAE, 2 from Saudi Arabia, 1 from Qatar, under a heading that said "25 Jobs".

A scraper that trusts the status code would charge you for 25 jobs from the wrong countries and never mention it.

Two things stop that here. The `country` input only accepts the 16 sites that exist, so a typo is refused before a single request is made. And on top of that, the Actor checks **the country of every job it actually found** against the country you asked for - so if GulfTalent ever starts answering a *valid* country with somebody else's jobs, you get **one unpaid row** saying so instead of a bill.

#### The salary is honest about how often it exists

Measured on 25 live UAE listings: **11 of 25 (44%) publish a salary**.

A job with no published salary comes back with `salaryMin: null` - **not `0`**. A zero quietly becomes "AED 0 per month" in a spreadsheet, which is worse than an empty cell. Currency (`AED`, …) and period (`month`, `year`, `hour`, …) are taken from what GulfTalent actually states; when it states nothing, `salaryPeriod` is `null` rather than a guess.

#### Every job, read from the structured data

Each job is taken from the **JSON-LD `JobPosting`** block GulfTalent publishes for search engines - present on **25 of 25** pages measured. That block is maintained for Google, so it survives visual redesigns that break HTML scrapers.

| field | how often it is filled (25 listings measured) |
|---|---|
| `title`, `company`, `city`, `region`, `country` | **100%** |
| `industry`, `employmentType` | **100%** |
| `datePosted`, `validThrough` (expiry) | **100%** |
| `directApply` | 75% |
| `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod` | 44% |

`validThrough` is the part most job feeds drop: you can tell a live vacancy from one that is about to close without opening it.

#### Text arrives readable

GulfTalent leaves HTML entities raw in its structured data - `IT, Hardware &amp; Networking` appeared in **15 of 25** listings. Some rows are **double-encoded**: one UAE listing carries `Dubai Healthcare City - &amp;#1571;&amp;#1605; …` for its Arabic address, which a single decoding pass leaves as `&#1571;`. Every text field here is decoded until it stops changing and stripped of HTML tags, so `industry` reads `IT, Hardware & Networking` and the address reads as Arabic.

Empty strings are turned into `null`. GulfTalent sends `postalCode: ""` on UAE listings; an empty string that looks like a value is worse than an honest blank.

#### Paging that actually pages

GulfTalent pages on the **path** (`.../software-developer/2`), not on `?page=`. Every query-string form - `?page=`, `?p=`, `?offset=`, `?start=`, `?pg=`, `?page_no=` - returns `HTTP 200` **and the same first 25 jobs**. Asking for a page past the last one silently serves the last page again.

This Actor uses the form that works, drops repeats, and stops when a page brings nothing new. Measured: `software-developer` in the UAE walks 5 pages to **115 unique jobs**, which is exactly the "115 Jobs" GulfTalent reports.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `country` | string | `uae` | One of the 16 country sites |
| `keyword` | string | `software-developer` | Hyphenated job title. Spaces are converted for you |
| `city` | string | — | `dubai`, `abu-dhabi`, `sharjah`… |
| `maxJobs` | integer | 25 | 1-1000. One job = one row = one charged event |
| `includeDescription` | boolean | `true` | Full advert text, HTML stripped |
| `useProxy` | boolean | `true` | Route through an Apify proxy. GulfTalent answers **HTTP 403** to a plain datacenter connection and **HTTP 200** to an ordinary home connection, measured on 2026-09-03 - so this is on by default |
| `maxPages` | integer | 0 | Safety cap; 0 = decide from `maxJobs` (25 per page) |
| `keywords` | array | — | Keep only jobs containing one of these words |
| `keywordMatch` | string | `any` | `any` or `all` |
| `excludeKeywords` | array | — | Drop jobs containing one of these |
| `companyNames` | array | — | Keep only these employers (partial match) |
| `minSalary` | integer | 0 | **A job with no published salary is kept** |
| `requireSalary` | boolean | `false` | Drop the ~56% that hide the salary |
| `postedWithinDays` | integer | 0 | 0 = no limit. **An unreadable date is kept** |
| `monitoringMode` | boolean | `false` | Return only jobs not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

#### Filters are applied before you are charged

The filters narrow the jobs already read - **nothing extra is fetched from GulfTalent** - and a job that does not match is never delivered and never charged. If a search returns jobs but none match your filters you get **one unpaid row saying exactly that** (`no-filter-match`), so you can tell "the filter was too tight" apart from "GulfTalent has nothing".

Two of them refuse to guess: a job with **no published salary is kept** by `minSalary` (an unknown salary is not a low one), and a job with an **unreadable posted date is kept** by `postedWithinDays`. Turn on `requireSalary` to drop the unknowns.

#### Monitoring mode turns this into a daily feed

Schedule it with `monitoringMode: true` and each run returns **only the jobs it has not returned before**. Already-seen jobs are skipped **before their pages are opened**, so a repeat run is fast as well as free. The first run returns everything and remembers it.

```json
{
  "country": "uae",
  "keyword": "software-developer",
  "city": "dubai",
  "maxJobs": 50
}
```

Measured on 2026-09-03 in the UAE: `software-developer` 115 jobs, `accountant` 138, `civil-engineer` 102. A job title GulfTalent does not have answers `HTTP 404`, and you get one unpaid row saying so.

***

### Output

One row per job:

```json
{
  "source": "gulftalent",
  "jobId": "616636",
  "url": "https://www.gulftalent.com/uae/jobs/microsoft-copilot-agent-developer-616636",
  "countrySlug": "uae",
  "title": "Microsoft Copilot Agent Developer",
  "company": "Connect Resources",
  "companyLogo": "https://www.gulftalent.com/images1/logos/listing/...png",
  "city": "Abu Dhabi",
  "region": "Abu Dhabi",
  "country": "UAE",
  "postalCode": null,
  "salaryMin": 10000,
  "salaryMax": 12000,
  "salaryPeriod": "month",
  "salaryCurrency": "AED",
  "employmentType": ["FULL_TIME"],
  "industry": "IT, Hardware & Networking",
  "datePosted": "2026-07-31T00:00:00+00:00",
  "validThrough": "2026-10-29T00:00:00+00:00",
  "directApply": true,
  "description": "We are seeking a Microsoft Copilot Agent Developer to …",
  "status": "ok",
  "scrapedAt": "2026-09-03T12:00:00.000Z"
}
```

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | GulfTalent has no jobs for this search. That is its answer, not a failure |
| `country-not-recognised` | GulfTalent answered 200 with other countries' jobs instead of an error |
| `unreadable` | The page could not be read; the HTTP status is in the note |

Each carries a `note` explaining what happened and the `searchUrl` it came from. **A job is charged only when its `JobPosting` was actually read** - no row is ever built by guessing.

***

### Pricing

Pay per job returned. Rows that report an empty search, an unrecognised country, or a page that could not be read are **not** charged. A job that appears twice is dropped before it is delivered, so it is never charged twice.

***

### Notes

- `hiringOrganization.sameAs` on GulfTalent repeats the company name rather than carrying a URL, so it is not exposed as a link. `companyLogo` is a real image URL.
- `robots.txt` was fetched on 2026-09-03: the `User-agent: *` section carries no `Disallow`.
- Only public pages are read. No login, no API key, no personal data beyond what the employer published in the advert.

# Actor input Schema

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

Which GulfTalent country site to search. Measured on 2026-09-03: a country GulfTalent does not recognise answers HTTP 200 with a mixed list of jobs from other countries instead of an error, so this Actor checks the country of every job it found and returns one unpaid row explaining the problem rather than charging you for the wrong country.

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

The job title to search for, as a hyphenated slug: `software-developer`, `accountant`, `civil-engineer`, `nurse`, `sales-manager`. Spaces are converted to hyphens for you. GulfTalent answers HTTP 404 for a title it does not have, so you get one row saying so instead of a silently unfiltered list. Measured on 2026-09-03 in the UAE: software-developer 115 jobs, accountant 138, civil-engineer 102.

## `city` (type: `string`):

Narrow to one city, for example `dubai`, `abu-dhabi`, `sharjah`. Leave empty for the whole country.

## `maxJobs` (type: `integer`):

How many jobs to return at most. One job = one row = one charged event. GulfTalent lists 25 jobs per page. A job that appears again is dropped, so it is never charged twice. Rows that say a search is empty, a country was not recognised, or a page could not be read are never charged.

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

Default: true. The full advert text with HTML stripped and HTML entities decoded. Turn it off for a smaller dataset - the price is the same either way.

## `useProxy` (type: `boolean`):

Default: true. Measured on 2026-09-03: GulfTalent answers HTTP 403 to a plain datacenter connection while returning HTTP 200 to an ordinary home connection, so the run goes through an Apify proxy. Turn it off only if you are running from a network GulfTalent already accepts.

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

Safety cap on how many listing pages to walk. 0 lets the Actor decide from Maximum jobs (25 per page). Asking GulfTalent for a page past the last one silently returns the last page again; this Actor stops when a page brings no new jobs.

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

Keep only jobs whose title, description, employer, industry or location contains one of these words. Case is ignored. Leave empty to keep everything. Nothing extra is fetched from GulfTalent - this narrows the jobs already read, and jobs that do not match are never charged.

## `keywordMatch` (type: `string`):

`any` keeps a job that contains at least one keyword. `all` keeps only jobs that contain every keyword.

## `excludeKeywords` (type: `array`):

Drop any job containing one of these words - for example an agency you do not want, or `commission only`. Applied after the keyword filter.

## `companyNames` (type: `array`):

Keep only jobs from employers whose name contains one of these (partial match, case ignored). Leave empty for all employers.

## `minSalary` (type: `integer`):

Keep only jobs paying at least this much, in the currency GulfTalent publishes (usually AED, usually per month). A job with no published salary is **kept**, not dropped - an unknown salary is not a low one. Measured on 2026-09-03: 44% of listings publish a salary.

## `requireSalary` (type: `boolean`):

Default: false. Turn it on to drop the roughly 56% of jobs that hide the salary.

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

Keep only jobs posted in the last N days. 0 means no limit. A job whose posted date cannot be read is **kept**, not dropped.

## `monitoringMode` (type: `boolean`):

Default: false. Turn it on and schedule this Actor daily: each run returns only the jobs it has not returned before, so you pay for new postings instead of the same list again. Already-seen jobs are skipped **before** their pages are opened, so repeat runs are fast as well as cheap.

## `resetMonitoringState` (type: `boolean`):

Default: false. Turn it on for a single run to forget which jobs were already returned and start monitoring again from scratch.

## Actor input object example

```json
{
  "country": "uae",
  "keyword": "software-developer",
  "city": "",
  "maxJobs": 25,
  "includeDescription": true,
  "useProxy": true,
  "maxPages": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "companyNames": [],
  "minSalary": 0,
  "requireSalary": false,
  "postedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per job: title, employer with logo, city and region, industry, employment type, posted date and expiry date, the country the job belongs to, and the salary as two numbers with its currency and period when the employer published one. Searches with no results, a country GulfTalent did not recognise, and pages that could not be read come back as their own rows and are not charged.

# 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 = {
    "country": "uae",
    "keyword": "software-developer"
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/gulftalent-jobs").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 = {
    "country": "uae",
    "keyword": "software-developer",
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/gulftalent-jobs").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 '{
  "country": "uae",
  "keyword": "software-developer"
}' |
apify call neverempty/gulftalent-jobs --silent --output-dataset

```

## MCP server setup

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

```

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/F7qmAx1k29egBOv11/builds/Ocfhcsd2bqgPPEiQf/openapi.json
