# jobs.ch Swiss Jobs Scraper (`zinin/jobs-ch-swiss`) Actor

Search jobs.ch (Switzerland) by keyword and get public job listings: title, company, location, employment type, posted date, URL. Unofficial, independent tool — not affiliated with, endorsed by, or sponsored by jobs.ch.

- **URL**: https://apify.com/zinin/jobs-ch-swiss.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (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.70 / 1,000 job founds

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/platform/actors/running/actors-in-store#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

## jobs.ch Swiss Jobs Scraper

jobs.ch is Switzerland's largest job board. This Actor searches it by keyword and returns one row per open role — title, company, location, employment type, posted date and a direct link — straight from the public, server-rendered search results. No login, no browser, no proxies.

### What you get

- **Live job listings, not a stale export.** The same job cards jobs.ch's own search page shows, for as many keywords as you give it in one run.
- **Optional full description.** Turn on one setting and this Actor also visits each job's own page for the real description text and a more precise location — off by default so a run stays fast.
- **Multiple keywords, one run.** Up to 10 search terms per call, each paginated and deduplicated independently.
- **Honest "no results."** A search that genuinely has nothing pushes a `found: false` row with no error — a search that failed to load pushes `found: false` with a real error message. The two never look the same.
- Runs on Apify: schedule it, monitor it, call it from the API or the MCP server, export to JSON, CSV or Excel, or push results straight into your own pipeline.

### How to run it

1. Click **Try for free** — no card needed on the free plan.
2. Paste your search terms into **Search keywords**, one per line — e.g. `marketing`, `software engineer`.
3. Press **Start**. Results appear in the dataset — read them in the UI, pull them from the API, or have a webhook push them onward.

### Pricing

Pay-per-event: **$0.005 per run start + $0.002 per job found**. No monthly seat, no minimum. 100 jobs cost about **$0.21**; 1,000 jobs about **$2.01**.

A keyword with no matches, or a page that failed to load, is still logged with `found: false` and the reason — and it is **not** charged for. You pay for jobs actually delivered, not for attempts.

### Input

| Field | Required | What it does |
|---|---|---|
| `keywords` | yes | Search terms, e.g. `marketing`, `software engineer`. Up to 10 per run. |
| `location` | no | Optional jobs.ch location filter (city or postal code), applied to every keyword. Leave empty to search all of Switzerland. |
| `max_items` | no | Job rows to collect per keyword, at most (default 30, max 200). |
| `max_pages` | no | Result pages to walk per keyword, at most (default 3, max 20; ~20 jobs/page). |
| `fetch_full_description` | no | Visit each job's own page for the real description and a more precise location. Slower — one extra request per job. Off by default. |

```json
{
    "keywords": ["marketing"],
    "max_items": 30,
    "max_pages": 3
}
```

### Output

One dataset row per job found. This is a real row from a real run (`VDjmy13MxbdsowQM9`, 2026-07-30, default settings, `fetch_full_description` off):

```json
{
    "found": true,
    "url": "https://www.jobs.ch/en/vacancies/detail/4c2c1609-3e60-4cf8-abdd-c9d21b694f75/",
    "job_id": "4c2c1609-3e60-4cf8-abdd-c9d21b694f75",
    "title": "Marketing Manager 80-100%",
    "company": "Wilhelm AG",
    "company_url": "https://www.jobs.ch/en/companies/142381-142381-wilhelm-ag/",
    "location": "CH",
    "country": "CH",
    "employment_type": "Permanent position",
    "posted_date": "2026-07-24T05:32:38.000Z",
    "salary_raw": null,
    "description": "We are looking for a Marketing Manager 80-100% to join our team.",
    "description_is_teaser": true,
    "source_board": "jobs-ch",
    "scraped_at": "2026-07-30T14:26:04.916Z",
    "matched_keyword": "marketing"
}
```

With `fetch_full_description: true`, the same job's `description` becomes the real posting text (not the short teaser), `description_is_teaser` becomes `false`, and `location`/`company_url` upgrade to more precise values when the job's own page has them (e.g. `location: "Roggwil, 9325"` instead of `"CH"`, `company_url` pointing at the employer's real site instead of its jobs.ch profile page).

| Field | What it means |
|---|---|
| `found` | Whether this row is a real job (`true`) or a notice/error row (`false`) |
| `url` | Direct link to the job posting — the unique key for this row |
| `job_id` | jobs.ch's own job identifier |
| `title` | Job title |
| `company` | Hiring company name |
| `company_url` | Link to the company (its own site when available, otherwise its jobs.ch profile) |
| `location` | Location as jobs.ch shows it — just the country unless `fetch_full_description` is on |
| `country` | Country code (always `CH` on this Actor) |
| `employment_type` | jobs.ch's own label, e.g. `"Permanent position"` |
| `posted_date` | ISO 8601 timestamp, or `null` if unparseable |
| `salary_raw` | Salary as published, or `null` — Swiss job ads essentially never publish one; this Actor never estimates or invents a figure |
| `description` | Job description, up to 600 characters; teaser text unless `fetch_full_description` is on |
| `description_is_teaser` | `true` when `description` is the short list-page snippet rather than the real posting text |
| `source_board` | Always `"jobs-ch"` — which board this Actor's row came from |
| `scraped_at` | When this Actor fetched the row |
| `matched_keyword` | Which of your search keywords produced this row |
| `partial` / `partial_reason` | Only present when `fetch_full_description` is on and that one job's own page failed to load — the row is still delivered and billed with the list-page data, just flagged as incomplete |

A keyword with no matches (only reliably reproducible via the `location` filter — a plain keyword search on jobs.ch is designed to always suggest *something*) pushes:

```json
{ "found": false, "error": "", "source_board": "jobs-ch", "scraped_at": "..." }
```

A keyword jobs.ch failed to answer pushes an error row with a non-empty `error` (e.g. `"http 403"`) — never the same shape as "no matches".

### API

Start a run with a bearer token and explicit JSON input:

```bash
curl -sS -X POST 'https://api.apify.com/v2/acts/zinin~jobs-ch-swiss/runs?waitForFinish=60' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"keywords":["marketing"],"max_items":30,"max_pages":3}'
```

Read Dataset rows using the returned `defaultDatasetId`:

```bash
curl -sS "https://api.apify.com/v2/datasets/$DEFAULT_DATASET_ID/items?clean=true&format=json" \
  -H "Authorization: Bearer $APIFY_TOKEN"
```

### MCP

For an Apify MCP client exposing the standard `call-actor` tool, send this exact payload:

```json
{
  "name": "call-actor",
  "arguments": {
    "actor": "zinin/jobs-ch-swiss",
    "input": {
      "keywords": [
        "marketing"
      ],
      "max_items": 30,
      "max_pages": 3
    }
  }
}
```

### Related Actors

Tools that cover the same Swiss/DACH hiring picture from a different angle — a company
not on Greenhouse/Lever/Ashby, or a wider region than jobs.ch alone.

| Actor | Use it when |
|---|---|
| [XING Jobs (DACH) Scraper](https://apify.com/zinin/xing-jobs) | Widening the same search to Germany and Austria, sourced straight from each job's own page rather than jobs.ch's keyword search. |
| [Job Postings Aggregator](https://apify.com/zinin/job-postings-aggregator) | The Swiss company you're checking does run a Greenhouse, Lever or Ashby board — pull its feed directly instead of searching jobs.ch. |
| [Company Hiring Radar](https://apify.com/zinin/company-hiring-radar) | You have one specific company in mind and want a scored hiring signal, not a keyword search across the whole market. |

### FAQ

**Does it need an API key / login?** No — it reads jobs.ch's public, server-rendered search results.

**Why is `salary_raw` almost always `null`?** Because jobs.ch almost never publishes one. This Actor never estimates or guesses a number — `null` means the source said nothing, not that the Actor failed to find it.

**Why does a nonsense search term still return jobs?** jobs.ch's own search is designed to never show an empty results page — an unmatched term falls back to unrelated "recommended" jobs rather than showing nothing. That is jobs.ch's behavior, not a bug in this Actor. To reliably get zero results (e.g. for testing), use the `location` filter with a place that doesn't exist.

**How fresh is the data?** Live at run time — every call fetches jobs.ch's current search results.

**Can I call it from an AI agent?** Yes — standard Apify Actor, callable from the Apify API, the SDK, or the Apify MCP server.

**What this is NOT.** It does not apply to jobs on your behalf, does not cover job boards outside jobs.ch, and does not invent a salary figure when the source has none.

Found a wrong result, or need a check we don't run? Open an issue on this Actor's page.

### Machine use

The Actor is callable through the Apify API, SDK, and Apify MCP server. The input and Dataset row are the machine-facing contract; `salary_raw` is `null` whenever jobs.ch itself publishes none — this Actor never estimates or invents a figure to fill the gap.

***

Built by [zinin](https://apify.com/zinin). Questions? Telegram [@timzinin](https://t.me/timzinin).

# Actor input Schema

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

Job search terms, one per jobs.ch search (e.g. "marketing", "software engineer"). Runtime cap: 10.

## `location` (type: `string`):

Optional jobs.ch location filter (city name or postal code), applied to every keyword. Leave empty to search all of Switzerland.

## `max_items` (type: `integer`):

How many job rows to collect per keyword, at most.

## `max_pages` (type: `integer`):

How many result pages to walk per keyword, at most (~20 jobs/page on jobs.ch).

## `fetch_full_description` (type: `boolean`):

Visit each job's detail page for the real description text and a more precise location (slower — one extra request per job). When off, description is the short teaser shown on the search results page.

## Actor input object example

```json
{
  "keywords": [
    "job"
  ],
  "max_items": 5,
  "max_pages": 1,
  "fetch_full_description": false
}
```

# Actor output Schema

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

API URL for the default dataset items 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 = {
    "keywords": [
        "job"
    ],
    "max_items": 5,
    "max_pages": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/jobs-ch-swiss").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": ["job"],
    "max_items": 5,
    "max_pages": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/jobs-ch-swiss").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "job"
  ],
  "max_items": 5,
  "max_pages": 1
}' |
apify call zinin/jobs-ch-swiss --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=zinin/jobs-ch-swiss",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/FohFC8v7VYNavfac6/builds/jh6zZfPSAMXCwGBUm/openapi.json
