# TopCV Scraper (`fanndev/topcv-scraper`) Actor

Search Vietnamese job listings on TopCV.vn by keyword and/or city and pull full job details (description, requirements, benefits, salary, employer info) -- no account or API key needed.

- **URL**: https://apify.com/fanndev/topcv-scraper.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Categories:** Jobs
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 results

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

## TopCV Scraper

Search Vietnamese job listings on TopCV.vn by keyword and/or city and get back full job details — description, requirements, benefits, salary, and employer info.

### Why use this actor

- No account, login, or API key required
- Full job description text (responsibilities, requirements, benefits), not just a title and a link
- Structured salary, location, and employer data straight from the job posting's own public data
- Automatic pagination up to the number of results you ask for
- Also works the other way: give it specific job links or IDs and it fetches full detail for exactly those

### How it works

Pick a `mode`:

1. **`search`** — searches by keyword and/or city and returns full job details for each match, with pagination handled automatically.
2. **`detail`** — fetches full detail for one or more specific job IDs/URLs you already have.

You don't need to manage any browsers or automatic retries — just provide a keyword, a city, or both.

### Input

**Keyword + city search (default mode):**

```json
{
  "mode": "search",
  "keyword": "python",
  "city": "Hà Nội",
  "maxResults": 50,
  "fetchFullDetails": true,
  "maxConcurrency": 4
}
```

**Job detail by URL/ID:**

```json
{
  "mode": "detail",
  "jobId": "https://www.topcv.vn/viec-lam/fullstack-software-engineer/2143657.html"
}
```

| Field | Type | Description |
|---|---|---|
| `mode` | string | `"search"` (default) or `"detail"`. |
| `keyword` | string | `search` mode — free-text search (e.g. `"python"`, `"kế toán"`, `"nhân viên bán hàng"`). Leave empty to list all currently active postings. |
| `city` | string | `search` mode — a Vietnamese province/city name or slug (e.g. `"Hà Nội"`, `"ha-noi"`, `"Hồ Chí Minh"`, `"Đà Nẵng"`). Combined with `keyword` when both are given. |
| `maxResults` | integer | `search` mode — max jobs to return, pagination followed automatically. Default 50. |
| `fetchFullDetails` | boolean | `search` mode — when on (default), each result includes full description/requirements/benefits/salary/employer data (one extra request per job). Turn off for a faster run that only needs the compact search-card fields (title, company, location, salary label, tags, posted date). |
| `jobId` / `jobIds` | string / array | `detail` mode — a job's numeric ID or its full `topcv.vn/viec-lam/...` URL. |
| `maxConcurrency` | integer | How many detail requests run in parallel. Default 4. |
| `proxyConfiguration` | object | Apify Proxy settings. Residential is on by default. |

### Output

**Search mode (`fetchFullDetails: true`, default)** — real output, truncated for readability:

```json
{
  "_input": "2143657",
  "_source": "S1-jsonld",
  "_scrapedAt": "2026-08-26T03:09:13Z",
  "@type": "JobPosting",
  "title": "Fullstack Software Engineer",
  "description": "<h2>Mô tả công việc</h2>\n<ul><li>Phân tích yêu cầu nghiệp vụ, phối hợp chặt chẽ với Product Owner...</li>...</ul>\n<h2>Yêu cầu ứng viên</h2>\n<ul><li>Tốt nghiệp đại học chuyên ngành Công nghệ thông tin...</li>...</ul>\n<h2>Quyền lợi được hưởng</h2>\n<ul><li>Thu nhập upto 45tr/tháng (Gross)</li>...</ul>",
  "identifier": { "name": "CÔNG TY CỔ PHẦN CMC OPENAI", "value": 239443 },
  "datePosted": "2026-08-26",
  "validThrough": "2026-09-25T23:59:59+07:00",
  "employmentType": "FULL_TIME",
  "hiringOrganization": {
    "name": "CÔNG TY CỔ PHẦN CMC OPENAI",
    "sameAs": "https://www.topcv.vn/cong-ty/cong-ty-co-phan-cmc-openai/239443.html",
    "logo": "https://cdn-new.topcv.vn/unsafe/https://static.topcv.vn/company_logos/....jpg"
  },
  "jobLocation": {
    "address": {
      "streetAddress": "Tòa nhà CMC – số 11 Duy Tân",
      "addressLocality": "Phường Cầu Giấy",
      "addressRegion": "Hà Nội",
      "addressCountry": "VN"
    }
  },
  "baseSalary": { "currency": "VND", "value": { "unitText": "MONTH", "maxValue": 45000000 } },
  "industry": "IT phần mềm",
  "occupationalCategory": "Nhân viên",
  "experienceRequirements": { "monthsOfExperience": 60 },
  "skills": "Java, OOP, Python, debug, Golang, CI/CD, RESTful API, unit test, Clean code, Design Patterns, SOLID principles, Node.js, ...",
  "totalJobOpenings": 3
}
```

**Search mode (`fetchFullDetails: false`)** — compact result, one request per page instead of one per job:

```json
{
  "_input": "keyword:python city:Hà Nội",
  "_source": "S1-html-search",
  "_scrapedAt": "2026-08-26T03:09:10Z",
  "job_id": "2277053",
  "title": "FULL-STACK AI DEVELOPER INTERN (Ruby / PHP / Python)_HCM/HN/DN",
  "url": "https://www.topcv.vn/viec-lam/full-stack-ai-developer-intern-ruby-php-python-hcm-hn-dn/2277053.html",
  "company_name": "Công ty TNHH ZIGExN VeNtura",
  "company_url": "https://www.topcv.vn/cong-ty/cong-ty-tnhh-zigexn-ventura/1036.html",
  "company_logo_url": "https://cdn-new.topcv.vn/unsafe/150x/https://static.topcv.vn/company_logos/....jpg",
  "salary_text": "Thoả thuận",
  "city": "Hà Nội & 2 nơi khác",
  "experience_text": "Không yêu cầu",
  "tags": ["AI Engineer", "IT - Phần mềm", "Tiếng Anh Đọc hiểu", "Tiếng Nhật JLPT N5", "Đại Học trở lên"],
  "posted_text": "Đăng1 tuần trước",
  "posted_updated_text": "Cập nhật 12 phút trước",
  "is_verified_employer": true
}
```

| Field | Type | Description |
|---|---|---|
| `title` | string | Job title. Both record shapes. |
| `description` | string | Full-detail records — HTML-formatted, combines description/requirements/benefits under `<h2>` sub-headings. |
| `baseSalary` | object | Full-detail records — structured `{currency, value: {unitText, maxValue?, minValue?}}`. When the employer marks salary negotiable, `value.value` holds the literal string `"Thoả thuận"` instead of a number. |
| `salary_text` | string | Compact records — the site's own displayed salary text (e.g. `"Tới 45 triệu"`, `"Thoả thuận"` = negotiable), not parsed into a number. |
| `hiringOrganization` / `company_name` | object / string | Employer name (+ profile URL, logo on full-detail records). |
| `jobLocation` / `city` | object / string | Full address (full-detail) or displayed city text (compact). |
| `skills` | string | Full-detail records — comma-separated skill/keyword list. |
| `totalJobOpenings` | integer | Full-detail records — open headcount for this posting. |
| `tags` | array | Compact records — job-function/skill/benefit tags shown on the result card. |

A record with `_error` instead means that lookup couldn't complete (e.g. `"_error": "not_found"` for an expired/nonexistent job ID, `"_error": "invalid_city"` for a city name that doesn't match TopCV's own province list).

### Known limits

- **Only two search filters are supported: `keyword` (free text) and `city` (Vietnamese province).** TopCV's search runs entirely through pretty "slug" URLs (there's no JSON search API to call) — no salary-range, experience-level, or employment-type filter could be reverse-engineered as a working combination.
- **`salary_text` on compact records is the site's own display string, not a parsed number** — many postings are marked "Thoả thuận" (negotiable) rather than disclosing a figure.
- **Result counts reflect the site's actual current job postings** — a job board naturally has fewer live postings at any moment than, say, a news archive (e.g. a narrow keyword like "python" nationwide can return well under 20 results).

# Actor input Schema

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

"search" runs a keyword/city job search and (by default) hydrates each result into a full job record. "detail" fetches full detail for specific job ID(s)/URL(s) directly.

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

"search" mode -- free-text job title/keyword search (e.g. "python", "kế toán", "nhân viên bán hàng"). Leave empty (with a city, or entirely) to list all currently active postings, newest first.

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

"search" mode -- optional. A Vietnamese province/city name or slug as used on topcv.vn, e.g. "Hà Nội", "ha-noi", "Hồ Chí Minh", "ho-chi-minh", "Đà Nẵng". Combined with "keyword" when both are given.

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

"search" mode -- maximum number of job listings to return. Pagination is followed automatically until this cap or the end of the result set.

## `fetchFullDetails` (type: `boolean`):

"search" mode -- when on (default), each search result is hydrated into a full job record (description, requirements, benefits, salary, employer info) with one extra request per job. Turn off for faster/cheaper runs that only need the compact search-result fields (title, company, location, salary label, tags, posted date).

## `jobId` (type: `string`):

"detail" mode -- a single job's numeric ID or the full topcv.vn/viec-lam/.../<id>.html URL.

## `jobIds` (type: `array`):

"detail" mode only -- multiple job IDs/URLs to fetch full detail for in one run.

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

How many detail requests to run in parallel (used in "search" mode when hydrating results, and in "detail" mode across multiple IDs).

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

Apify Proxy configuration. Residential is on by default.

## Actor input object example

```json
{
  "mode": "search",
  "keyword": "python",
  "city": "Hà Nội",
  "maxResults": 50,
  "fetchFullDetails": true,
  "jobId": "https://www.topcv.vn/viec-lam/fullstack-software-engineer/2143657.html",
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

All scraped job search / detail records produced by this run.

# 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",
    "keyword": "python",
    "city": "Hà Nội",
    "maxResults": 50,
    "fetchFullDetails": true,
    "jobId": "https://www.topcv.vn/viec-lam/fullstack-software-engineer/2143657.html",
    "maxConcurrency": 4,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/topcv-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",
    "keyword": "python",
    "city": "Hà Nội",
    "maxResults": 50,
    "fetchFullDetails": True,
    "jobId": "https://www.topcv.vn/viec-lam/fullstack-software-engineer/2143657.html",
    "maxConcurrency": 4,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/topcv-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",
  "keyword": "python",
  "city": "Hà Nội",
  "maxResults": 50,
  "fetchFullDetails": true,
  "jobId": "https://www.topcv.vn/viec-lam/fullstack-software-engineer/2143657.html",
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call fanndev/topcv-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/topcv-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/yV9YrjS6Vf2r3tthp/builds/nZTtVPh7MDjAJoI66/openapi.json
