# eJobs.ro Job Listings Scraper (`bovi/ejobs-ro-scraper`) Actor

Extract Romanian job listings from the public eJobs JSON API, including job taxonomy, company and location metadata, and optional full job descriptions.

- **URL**: https://apify.com/bovi/ejobs-ro-scraper.md
- **Developed by:** [Vitalii Bondarev](https://apify.com/bovi) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 ejobs.ro job listings scrapers

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

## eJobs.ro Scraper

Scrape public job listings from eJobs Romania.

This actor reads the public `api.ejobs.ro` JSON API with Python `curl_cffi`. It does not launch or control a browser, does not use Playwright or Selenium, and does not scrape the Cloudflare-protected eJobs website or its HTML pages. All job and taxonomy data comes from public JSON API responses.

### What it collects

The actor can collect listing-level job data such as:

- Job ID, title, URL, and publication dates
- Company name and company identifiers
- City, county, country, and remote/work-model information
- Employment type, career level, and domain/category data
- Salary information when published by the employer
- Listing description data
- Optional full detail data from the public job-detail API endpoint
- Normalized taxonomy values for locations, domains, job types, career levels, and work models

Each job is stored as one dataset record.

### Quick start

Use a minimal input to collect jobs matching a search phrase:

```
{
  "search": "python developer",
  "maxItems": 50
}
```

To fetch the richer job-detail response for every collected job:

```
{
  "search": "python developer",
  "maxItems": 50,
  "fetchDetails": true
}
```

To search and apply local filters:

```
{
  "search": "developer",
  "maxItems": 100,
  "fetchDetails": true,
  "filters": {
    "locations": ["București"],
    "jobTypes": ["Full time"],
    "careerLevels": ["Mid-Level"],
    "remoteOnly": true
  }
}
```

### Main input

| Input | Type | Description |
|---|---:|---|
| `search` | string | Search phrase sent to the public eJobs API where supported. Use an empty value to browse available jobs without a text query. |
| `maxItems` | integer | Maximum number of matching job records to emit. This is a record limit, not a page limit. |
| `startPage` | integer | Optional API page from which to begin pagination. Pagination normally starts at page 1. |
| `maxPages` | integer | Optional maximum number of API pages to request. Use this to put an additional bound on a broad search. |
| `fetchDetails` | boolean | When `true`, request the public detail endpoint for each matched job and enrich the listing record with available detail data. |
| `filters` | object | Optional client-side filters applied to normalized listing data. See [Client-side filters](#client-side-filters). |
| `proxyConfiguration` | object | Optional Apify Proxy configuration. If omitted, the actor uses a Romanian `RESIDENTIAL` proxy configuration by default. |

The actor accepts normal Apify proxy settings. Supplying a custom proxy configuration overrides the default Romanian residential proxy selection.

### Proxy behavior

The default network configuration uses an Apify `RESIDENTIAL` proxy with Romanian routing.

Romanian residential routing is the default because eJobs is a Romanian job board and API behavior may differ by region. A proxy is still an HTTP transport setting only: the actor calls the JSON API directly and does not attempt to visit or bypass the Cloudflare-protected website.

### Client-side filters

The API search response can be broad or incomplete for some combinations of criteria. For predictable results, the actor applies the following filters locally after normalizing each listing.

There are no hidden client-side filters beyond the fields listed below.

| Filter field | Accepted value | Behavior |
|---|---|---|
| `includeKeywords` | string or array of strings | Keeps jobs whose searchable text contains the supplied keyword values. Searchable text includes title, company, location labels, and available description text. |
| `excludeKeywords` | string or array of strings | Rejects jobs whose searchable text contains any excluded keyword. |
| `includeCompanies` | string or array of strings | Keeps jobs whose company name matches one of the supplied company values. |
| `excludeCompanies` | string or array of strings | Rejects jobs whose company name matches an excluded value. |
| `locations` | string or array | Keeps jobs matching one or more normalized location names or location IDs. |
| `counties` | string or array | Keeps jobs matching one or more Romanian county names or IDs when county data is available. |
| `domains` | string or array | Keeps jobs matching one or more normalized job domains/categories or their IDs. |
| `jobTypes` | string or array | Keeps jobs matching one or more normalized employment types, such as full-time, part-time, internship, or contract values exposed by the API. |
| `careerLevels` | string or array | Keeps jobs matching one or more normalized career levels. |
| `workModels` | string or array | Keeps jobs matching one or more normalized work models, such as onsite, hybrid, or remote when supplied by eJobs. |
| `remoteOnly` | boolean | When `true`, keeps only jobs marked as remote or having a remote work model. |
| `salaryMin` | number | Keeps jobs whose published salary range can satisfy the requested minimum salary. Jobs without usable salary data do not match this filter. |
| `salaryMax` | number | Keeps jobs whose published salary range can satisfy the requested maximum salary. Jobs without usable salary data do not match this filter. |
| `salaryCurrency` | string | Restricts salary matching to the supplied currency code when salary data includes a currency. |
| `withSalary` | boolean | When `true`, keeps only jobs with published salary data. When `false`, keeps only jobs without published salary data. |
| `postedAfter` | ISO date or datetime string | Keeps jobs published on or after the supplied value. |
| `postedBefore` | ISO date or datetime string | Keeps jobs published on or before the supplied value. |

#### Filter matching rules

- Text matching is case-insensitive.
- A list within one filter is treated as an OR condition. For example, `locations: ["București", "Cluj-Napoca"]` accepts either location.
- Different filter groups are combined with AND. A job must satisfy every supplied filter group.
- Exclusion filters take precedence over inclusion filters.
- Taxonomy filters can use normalized names or IDs when those IDs are available in the API taxonomy.
- `maxItems` is applied after client-side filtering. The actor continues paginating until it has emitted the requested number of matching jobs or there are no more results.

Example:

```
{
  "search": "software engineer",
  "maxItems": 100,
  "filters": {
    "includeKeywords": ["python", "backend"],
    "excludeKeywords": ["senior"],
    "locations": ["București", "Cluj-Napoca"],
    "jobTypes": ["Full time"],
    "careerLevels": ["Mid-Level"],
    "withSalary": true
  }
}
```

### Taxonomy normalization

eJobs API responses may contain internal IDs, labels, partial objects, or differently shaped values depending on the endpoint. The actor resolves these into consistent taxonomy fields where possible.

Taxonomy is fetched once per run and reused for all listing and detail records in that run. It is not fetched separately for every job.

Representative normalized taxonomy fields include:

| Output field | Description |
|---|---|
| `locations` | Normalized location objects and/or labels associated with the job. |
| `locationIds` | API location identifiers when available. |
| `cities` | City names derived from the job location data. |
| `counties` | County names or identifiers when exposed by the API. |
| `domains` | Normalized job domain/category objects or labels. |
| `domainIds` | API domain/category IDs when available. |
| `jobTypes` | Normalized employment-type labels and IDs. |
| `careerLevels` | Normalized career-level labels and IDs. |
| `workModels` | Normalized work-model values, including remote, hybrid, or onsite where supplied. |
| `isRemote` | Boolean remote indicator derived from the available API data. |

The actor preserves useful listing-level values even when a taxonomy item cannot be resolved. Taxonomy labels should be treated as values supplied by eJobs and may change over time.

### Optional detail fetching

Set `fetchDetails` to `true` to request the public job-detail API response for each matched listing.

Detail responses can provide richer information than the listing feed, including fuller descriptions, application information, salary details, employer data, or additional job metadata when eJobs exposes them.

The actor deliberately treats a failed detail request as non-fatal:

1. The job is first collected from the listing API.
2. The actor attempts the individual detail request when `fetchDetails` is enabled.
3. If that detail request succeeds, available detail values enrich the record.
4. If an individual detail request fails, the actor still emits the job using the listing data.

A failed individual detail request therefore does not discard an otherwise valid listing and does not stop the rest of the run.

### Description fields

When description content is available, the actor exposes both markup and readable text forms:

| Field | Description |
|---|---|
| `descriptionHtml` | Description markup supplied by the public API. Preserve this field when formatting and links matter. |
| `descriptionText` | Plain-text version of the description for search, analysis, exports, and language processing. |
| `shortDescription` | Shorter listing-level summary when supplied separately by the API. |

If `fetchDetails` is disabled, description fields may be limited to what is present in the listing response. With detail fetching enabled, the actor uses the richer detail description when it is available.

### Pagination

The actor paginates through the public eJobs listing API.

Pagination behavior:

- Pages are requested in ascending order, beginning with `startPage` or page 1.
- The actor stops after `maxItems` matching records have been emitted.
- If `maxPages` is set, the actor also stops after that many requested pages.
- The actor stops when the API has no additional results, returns an empty page, or indicates the end of the result set.
- Client-side filters are evaluated for every listing before it counts toward `maxItems`.
- Repeated job IDs are de-duplicated where possible so the same job is not intentionally emitted multiple times during one run.

A broad query with restrictive local filters may require more API pages than the number of records ultimately emitted.

### Representative output

The exact set of fields depends on the API response and whether detail fetching is enabled. A representative record can contain fields like the following:

```
{
  "id": "job-id",
  "title": "Python Developer",
  "url": "https://www.ejobs.ro/user/locuri-de-munca/python-developer/...",
  "companyName": "Example Company",
  "companyId": "company-id",
  "locations": [
    {
      "id": "location-id",
      "name": "București"
    }
  ],
  "cities": ["București"],
  "counties": ["București"],
  "domains": [
    {
      "id": "domain-id",
      "name": "IT Software"
    }
  ],
  "jobTypes": ["Full time"],
  "careerLevels": ["Mid-Level"],
  "workModels": ["Hybrid"],
  "isRemote": false,
  "salary": {
    "min": 8000,
    "max": 12000,
    "currency": "RON"
  },
  "publishedAt": "2025-01-01T10:00:00Z",
  "expiresAt": "2025-01-31T23:59:59Z",
  "descriptionHtml": "Description markup returned by the API",
  "descriptionText": "Readable job description text",
  "detailFetched": true,
  "scrapedAt": "2025-01-01T12:00:00Z"
}
```

Common fields include:

| Field | Description |
|---|---|
| `id` | Stable eJobs job identifier when provided. |
| `title` | Job title. |
| `url` | Public job URL when available. |
| `companyName` | Employer name. |
| `companyId` | Employer identifier when available. |
| `locations`, `cities`, `counties` | Normalized geographic information. |
| `domains`, `jobTypes`, `careerLevels`, `workModels` | Normalized taxonomy values. |
| `isRemote` | Derived remote-work indicator. |
| `salary` | Published salary range and currency when available. |
| `publishedAt`, `expiresAt` | Job publication and expiry timestamps when supplied. |
| `descriptionHtml` | API-provided description markup. |
| `descriptionText` | Plain-text description. |
| `detailFetched` | Indicates whether the job-detail request successfully enriched the listing. |
| `scrapedAt` | Timestamp at which the actor processed the record. |

Fields can be absent or null when eJobs does not provide the corresponding information.

### Free-plan preview

On the Apify free plan, the actor provides a preview of up to 10 job records.

The 10-record free-plan preview is a plan limit. It is not a pagination setting and it does not change the actor’s filtering or normalization behavior. A request with a larger `maxItems` value can still be constrained by the record availability allowed by the current Apify plan.

### Pricing and PPE charging

The actor charges one PPE for each successfully emitted job record.

A job record is chargeable only after it has been successfully produced and emitted to the dataset. API requests, taxonomy loading, empty pages, failed detail requests, filtered-out listings, and jobs that are not emitted do not create a job-record PPE charge.

When `fetchDetails` is enabled, a successful detail request does not create a second PPE charge for the same job. The unit is one successfully emitted job record.

### Local fixture test

The repository includes a fixture-based test for validating parsing and normalization without relying on the live eJobs API.

Install the project dependencies, then run:

```
python -m pytest -q
```

The fixture test should use saved API payloads from the test fixtures and should not require a browser, a live eJobs account, or access to the Cloudflare-protected website. This makes it suitable for local development and regression testing when the public API is unavailable or rate-limited.

### Limitations

- The actor depends on public `api.ejobs.ro` responses. The API is controlled by eJobs and can change without notice.
- Only jobs and fields exposed by the public API can be collected.
- The actor does not access private employer data, authenticated user data, application submissions, or hidden jobs.
- Listing data can be incomplete. Salary, work model, county, company metadata, dates, and descriptions are not guaranteed for every job.
- Detail fetching can improve data quality but is not guaranteed. If an individual detail request fails, the actor falls back to the listing data for that job.
- A job can disappear, expire, or change while a paginated run is in progress.
- Taxonomy labels and IDs can be added, removed, renamed, or returned inconsistently by the upstream API.
- Salary values may use different currencies, ranges, gross/net conventions, or free-form employer text. Salary filtering only works when values can be interpreted reliably.
- Public API availability, rate limits, regional behavior, proxy availability, and network failures can affect throughput and completeness.
- This actor is designed for job-data collection and analysis. Users remain responsible for complying with applicable laws, eJobs terms, privacy obligations, and Apify platform rules.

# Actor input Schema

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

Optional keyword used for searching jobs. Filters are applied after records are normalized.

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

When enabled, fetching details adds the full job description, ideal-candidate description, and company description. Filters are applied after records are normalized.

## `maxResults` (type: `integer`):

Maximum number of job records to return.

## `cities` (type: `array`):

Optional city filters. Names or numeric IDs are accepted. Filters are applied after records are normalized.

## `departments` (type: `array`):

Optional department filters. Names or numeric IDs are accepted. Filters are applied after records are normalized.

## `industries` (type: `array`):

Optional industry filters. Names or numeric IDs are accepted. Filters are applied after records are normalized.

## `careerLevels` (type: `array`):

Optional career-level filters. Names or numeric IDs are accepted. Filters are applied after records are normalized.

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

Optional contract-type filters. Names or numeric IDs are accepted. Filters are applied after records are normalized.

## `maxAgeDays` (type: `integer`):

Optional maximum age of jobs in days. This filter is applied after records are normalized.

## `verifiedOnly` (type: `boolean`):

Return only verified jobs. This filter is applied after records are normalized.

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

Enables Apify Proxy with the RESIDENTIAL group and Romania as the proxy country.

## Actor input object example

```json
{
  "keyword": "",
  "fetchDetails": true,
  "maxResults": 100,
  "verifiedOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "RO"
  }
}
```

# Actor output Schema

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

Dataset containing eJobs.ro job records (id, title, company, location, contractTypes, careerLevels, departments, industries, publishDate, description, idealCandidate, companyDescription, url, scrapedAt).

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/ejobs-ro-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("bovi/ejobs-ro-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 '{}' |
apify call bovi/ejobs-ro-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,bovi/ejobs-ro-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/B2Yh98LkI9bihqS20/builds/7ngJQAEeCt0icgsWA/openapi.json
