Jobs.ch & Jobup.ch Scraper - Swiss Jobs, Salaries & Companies avatar

Jobs.ch & Jobup.ch Scraper - Swiss Jobs, Salaries & Companies

Pricing

$1.50 / 1,000 jobs

Go to Apify Store
Jobs.ch & Jobup.ch Scraper - Swiss Jobs, Salaries & Companies

Jobs.ch & Jobup.ch Scraper - Swiss Jobs, Salaries & Companies

Scrape Swiss job ads from jobs.ch and jobup.ch by keyword, location or URL: title, company, location, workload, employment type, salary when published, date. Schedule new-job alerts to Discord, Telegram, Slack or any webhook. No login, no recruiter contact data.

Pricing

$1.50 / 1,000 jobs

Rating

0.0

(0)

Developer

Luke Hunter

Luke Hunter

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

2

Monthly active users

10 days ago

Last modified

Share

For recruiters, HR analysts and job seekers who want Swiss job data without copy-pasting. Give it keywords (and optionally a place such as Zürich or Lausanne), or paste your own jobs.ch / jobup.ch search URLs, and it returns every job ad on the result pages: title, company, location, workload, employment type, the salary when the ad publishes one, and the posting date. It reads the same public search pages a signed-out visitor sees - no login, no session, no recruiter contact data. Turn on new-job alerts (onlyNewSinceLastRun) and run it on a schedule to get only the jobs you have not seen before.

Pay-per-result: $0.0015 per job — 1,000 jobs = $1.50.

Quick start (1 minute)

  1. Open the Input tab and enter one or more keywords, e.g. python.
  2. Pick the Site: jobs.ch (default) or its French-language sister site jobup.ch.
  3. Click Start. Export the dataset to CSV/Excel/JSON, or pull it through the API shown below.
{
"searchTerms": ["python"],
"site": "jobs.ch",
"maxPagesPerSearch": 1,
"maxItems": 20
}

That prefilled example returns 20 jobs, finishes in well under a minute and costs $0.03.

How it works

jobs.ch and jobup.ch (both run by JobCloud) embed the full search result in the HTML of every public search page. This Actor:

  1. Checks the site's robots.txt for the exact path and query of every page it is about to read, and skips that search (saying why in the run's status message) if it is disallowed. It never fetches a job detail page, which jobs.ch's robots.txt disallows.
  2. Pages through the results - 20 jobs per page - until the site's own last page, your maxPagesPerSearch, or your maxItems.
  3. Turns each ad into one flat dataset row. Anything an ad does not publish is null; nothing is guessed or estimated.
  4. Reads politely: one honest, self-identifying request about every 3 seconds, no proxies, no cookies, no disguised browsers. A block or challenge page is reported in the run's status message, never worked around, and pages already collected are kept.

Use cases

  • Swiss job market and salary data: export every "data engineer", "pflegefachperson" or "comptable" ad with company, canton, workload and - where the ad publishes it - the salary range, for hiring-market research or salary benchmarking.
  • Competitor hiring tracker: search a company or product name on a schedule to see what Swiss employers are hiring for, how many roles are open and how fast new ones appear.
  • Switzerland job alerts feed: save a keyword + location (for example python in Zürich, or ingénieur on jobup.ch for French-speaking Switzerland), schedule the Actor, and pipe only the new jobs into Slack, e-mail or a spreadsheet.

New-job alerts: get only jobs you have not seen

Turn on onlyNewSinceLastRun and put the Actor on a schedule: every run after the first delivers (and charges for) only jobs it has not delivered before for that exact search.

{
"searchTerms": ["python"],
"location": "Zürich",
"site": "jobs.ch",
"onlyNewSinceLastRun": true,
"maxPagesPerSearch": 2,
"maxItems": 100
}
  1. Click Schedule on the run page (or create one under Schedules in the Apify Console). Daily works well for most searches.
  2. The first scheduled run is a baseline: it delivers every job currently listed (it is all new to you) and remembers it. You are charged normally for that run.
  3. Every later run delivers only jobs not seen before for this exact search.
  4. Add a Webhook for ACTOR.RUN.SUCCEEDED pointing at Apify's Slack/e-mail integration (or a Zapier/Make step) to be told when new jobs arrive.

The status message tells you what happened: "First run for 1 search: 39 job(s) delivered and remembered; next runs return only new ones." on the baseline, then "3 new job(s) since last run across 1 search (36 already seen)." later. A run that finds nothing new still succeeds with an empty dataset - that is the point of the mode.

Each keyword and each search URL is its own independent watch (keyed on keyword + location + site, or the exact URL), so several saved searches in one task never mix up their histories. stateStoreName only needs changing to isolate or reset a watch.

Get new Swiss job alerts in Discord, Telegram, Slack, Teams, Google Chat, your phone (ntfy/Pushover) or any app (webhook)

Want a jobs.ch or jobup.ch job alert in Discord, Telegram or on your phone instead of checking a dataset? Add one or more destinations in the Actor's Notifications (optional) section and each run sends the jobs it delivered. It is a monitor that notifies you: it only reads public search pages, and it never applies to a job, contacts a recruiter or logs in to jobs.ch or jobup.ch.

ChannelFill inWhat you get
DiscorddiscordWebhookUrlOne card per job, 10 per message
TelegramtelegramBotToken + telegramChatIdUp to 10 jobs per message, one line each
SlackslackWebhookUrlBlock Kit message, up to 10 jobs per message
Mattermost, Rocket.ChatslackCompatibleWebhookUrlSlack-style incoming webhook: a Markdown list (these apps do not render Slack Block Kit)
Microsoft TeamsteamsWebhookUrlAdaptive Card from a Teams Workflows webhook, up to 10 jobs per card
Google ChatgoogleChatWebhookUrlCard in a space, up to 10 jobs per message
ntfy (phone push, no account)ntfyTopic (optional ntfyServer, ntfyToken)A push per job, up to 10, then one "...and N more"
Pushover (phone push)pushoverUserKey + pushoverAppTokenA push per job, up to 10, then one "...and N more"
Any app: Zapier, Make, n8n, Pipedream, IFTTT, Home AssistantwebhookUrlOne JSON POST per run with every delivered row

Fill in as many as you like; each channel works on its own and a failing one never stops the others.

Setup, channel by channel

  • Discord (1 minute): Server Settings → Integrations → Webhooks → New Webhook → pick the channel → Copy Webhook URL.
  • Telegram (2 minutes): chat with @BotFather, send /newbot, paste the token into telegramBotToken. Add the bot to the chat or channel (administrator for a channel; for a chat with yourself press Start on the bot). telegramChatId is a numeric id (123456789, or -1001234567890 for a group or channel) or a public @channelusername.
  • Slack: at api.slack.com/apps create an app (From scratch) → Incoming Webhooks → switch it on → Add New Webhook to Workspace → pick the channel → copy the https://hooks.slack.com/services/... URL.
  • Mattermost or Rocket.Chat: create an incoming webhook in the app and paste its public https:// URL into slackCompatibleWebhookUrl. It must be reachable from the internet: localhost, private IP addresses and .local / .internal names are rejected.
  • Microsoft Teams: in the channel open ... → Workflows → Post to a channel when a webhook request is received → finish the wizard and copy the URL (a host ending in .logic.azure.com or .powerplatform.com). The old "Office 365 connector" webhooks are retired and not supported.
  • Google Chat: in the space open Apps & integrations → Webhooks → Add webhook, name it and copy the https://chat.googleapis.com/v1/spaces/... URL (needs a Google Workspace account that allows webhooks).
  • ntfy (free, no account): install the ntfy app (or open ntfy.sh), subscribe to a topic with a long random name, and put the same name in ntfyTopic. Anyone who knows the topic can read it, so make it unguessable. Own server or a protected topic: set ntfyServer (public https:// address) and ntfyToken.
  • Pushover: copy your user key from pushover.net into pushoverUserKey, create an application at pushover.net/apps/build and put its API token in pushoverAppToken. Pushover itself is a separate paid app after its trial: check their pricing.
  • Any app (webhook): paste the catch-hook URL from Zapier, Make, n8n, Pipedream, IFTTT or Home Assistant into webhookUrl. It must be a public https:// address.

The recipe: a Swiss job alert in five steps

  1. Set your search: searchTerms with an optional location and site, or a jobs.ch / jobup.ch search URL with your filters in startUrls.
  2. Turn on onlyNewSinceLastRun.
  3. Add your destination(s) from the table above.
  4. Run it once. The first run is the baseline and sends the jobs currently listed (up to maxNotifications).
  5. Create a Schedule (daily works for most searches). From then on you only hear about jobs it has not delivered before.
{
"searchTerms": [
"python"
],
"location": "Zürich",
"site": "jobs.ch",
"onlyNewSinceLastRun": true,
"maxItems": 100,
"discordWebhookUrl": "https://discord.com/api/webhooks/<id>/<token>",
"ntfyTopic": "your-long-random-topic-name",
"maxNotifications": 25
}

The generic webhook (webhookUrl)

Each run sends one POST per 100 delivered rows (so usually one) with this body, followed by whatever your tool does with it:

{
"source": "apify",
"actor": "jobs-ch-scraper",
"runId": "HG7ML7M8z78YcAPEB",
"count": 2,
"total": 2,
"part": 1,
"parts": 1,
"items": [
{
"...": "the full dataset rows"
}
]
}

count is the number of rows in this POST, total the number delivered in the run, and part / parts number the POSTs when a run delivers more than 100 rows. maxNotifications does not cap the webhook: your tool gets every delivered row. Your endpoint should answer with a 2xx status; redirects are not followed and a failure is reported in the run status, never retried in a loop.

What each message contains

Each job is announced as New job: <title> at <company> — <location> with the link, the published salary when the ad has one, the workload (for example 80-100%), employment type, canton, publication date, and the site and search term it came from. Anything the ad does not publish is left out. Discord gets a card per job, Telegram one line per job, Slack Block Kit sections, Teams an Adaptive Card, Google Chat a card, and ntfy/Pushover a push that opens the job. Link previews are off, and titles are escaped so a title can never inject formatting or mentions.

What to expect

  • Only delivered jobs are sent. A run that finds nothing new sends nothing, so there is no "no new jobs" spam. Rows the run could not deliver (for example because your spending limit was reached) are not sent.
  • Without onlyNewSinceLastRun, every delivered row is sent, so a scheduled plain run would repeat the same jobs: use alert mode for alerts (the message then says "New job"; a plain run says "Job").
  • maxNotifications (default 25, max 100) caps how many jobs go to chat and phone channels per run (the webhook is not capped). The rest are still in the dataset and the status message says so. ntfy and Pushover send at most 10 individual pushes, then one "...and N more" summary.
  • Cost: notifications are free. You pay only the usual price for delivered jobs, exactly as without alerts. There is no extra charge for any channel.
  • Failures never fail the run. A deleted webhook or an unreachable service leaves the run successful and the dataset complete, and the status message says what happened, for example Notified: Discord 12, Slack 12, ntfy 10 (+summary). Teams failed (HTTP 404) - check the webhook URL. The logs show the channel name and HTTP status only, never a URL or token.
  • Your URLs, tokens and ntfy topic are stored as secret inputs. Anyone holding a webhook URL can post to that channel, so treat it like a password.
  • It is a monitor that notifies you. It never applies to jobs, contacts recruiters or logs in to jobs.ch or jobup.ch. The messages go only to the destinations you set up.
  • Nothing changes by default. With no destination set, a run behaves exactly as before and the dataset rows are identical.

Input

{
"searchTerms": ["python"],
"location": "Zürich",
"site": "jobs.ch",
"startUrls": [],
"maxPagesPerSearch": 2,
"maxItems": 100,
"onlyNewSinceLastRun": false,
"stateStoreName": "jobs-ch-scraper-seen-jobs"
}
FieldTypeDefaultDescription
searchTermsstring[](none)Keywords to search for, e.g. "python". Each is searched on the chosen site. Up to 50 per run.
locationstring(none)Optional place name that narrows every keyword search, e.g. "Zürich". Ignored for startUrls.
site"jobs.ch" | "jobup.ch""jobs.ch"Which board to search for the keywords.
startUrlsstring[](none)Full jobs.ch or jobup.ch search URLs (any language path), e.g. https://www.jobs.ch/en/vacancies/?term=python&location=bern. Filters already in the URL are kept; only pagination is added. Up to 50 per run. At least one of searchTerms or startUrls is required.
maxPagesPerSearchinteger21-10. Result pages fetched per keyword or URL; 20 jobs per page.
maxItemsinteger1001-5,000. Hard cap on jobs delivered across the whole run - your cost cap.
onlyNewSinceLastRunbooleanfalseNew-job alerts mode - see above.
stateStoreNamestringjobs-ch-scraper-seen-jobsName of the store that remembers what each alert-mode search has delivered. Letters, numbers and hyphens only.
discordWebhookUrlstring (secret)(none)Discord webhook URL. Must look like https://discord.com/api/webhooks/<id>/<token>. Empty means no Discord messages. See the alerts section above.
telegramBotTokenstring (secret)(none)Telegram bot token from @BotFather. Needs telegramChatId.
telegramChatIdstring(none)Telegram chat to post to: a numeric chat id or @channelusername. The bot must be added to that chat or channel. Needs telegramBotToken.
slackWebhookUrlstring (secret)(none)Slack incoming webhook, https://hooks.slack.com/services/....
slackCompatibleWebhookUrlstring (secret)(none)Mattermost / Rocket.Chat incoming webhook (public https:// URL; Markdown list, no Block Kit).
teamsWebhookUrlstring (secret)(none)Microsoft Teams Workflows webhook (host ending .logic.azure.com or .powerplatform.com).
googleChatWebhookUrlstring (secret)(none)Google Chat space webhook, https://chat.googleapis.com/v1/spaces/....
ntfyTopicstring (secret)(none)ntfy topic name; setting it turns ntfy phone pushes on. Pick a long, unguessable name.
ntfyServerstringhttps://ntfy.shntfy server. Only change it for your own public https:// ntfy server.
ntfyTokenstring (secret)(none)Optional ntfy access token for a self-hosted server or protected topic. Needs ntfyTopic.
pushoverUserKeystring (secret)(none)Pushover user key. Needs pushoverAppToken.
pushoverAppTokenstring (secret)(none)Pushover application API token. Needs pushoverUserKey.
webhookUrlstring (secret)(none)Generic JSON webhook (Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant): every delivered row, 100 per POST. Public https:// only.
maxNotificationsinteger251–100. Most jobs sent to chat and phone channels per run (the webhook is not capped). Extra jobs are still in the dataset. Only used when a destination is set.

Output fields

CategoryFields
Identityid, url, site
Jobtitle, company, location, canton, publishedAt
ContractemploymentType, workload, workloadMin, workloadMax
Salary (only when published)salaryText, salaryMin, salaryMax, salaryCurrency, salaryPeriod
Search contextsearchTerm, page, position, scrapedAt

Missing values are returned as null. Nothing is invented.

Output example

A real row from a jobs.ch search for python (the ad publishes no salary, so all salary fields are null):

{
"id": "384bbefd-1193-4545-b36f-72cdbd1ffa94",
"url": "https://www.jobs.ch/en/vacancies/detail/384bbefd-1193-4545-b36f-72cdbd1ffa94/",
"title": "Senior Fullstack Engineer (C#, Angular, Python) (a)",
"company": "ERNI Schweiz AG",
"location": "Zürich",
"canton": "ZH",
"employmentType": "Permanent position",
"workload": "100%",
"workloadMin": 100,
"workloadMax": 100,
"salaryText": null,
"salaryMin": null,
"salaryMax": null,
"salaryCurrency": null,
"salaryPeriod": null,
"publishedAt": "2026-06-25T15:20:58+02:00",
"site": "jobs.ch",
"searchTerm": "python",
"page": 1,
"position": 1,
"scrapedAt": "2026-10-03T00:00:00.000Z"
}

And a real row whose ad does publish a salary range (jobs.ch, page 2):

{
"id": "a6a10b48-c5ce-4392-93ba-0aefae34d436",
"url": "https://www.jobs.ch/en/vacancies/detail/a6a10b48-c5ce-4392-93ba-0aefae34d436/",
"title": "Embedded Systems Engineer (m/f, 100%)",
"company": "ROOMZ SA",
"location": "Bern, Fribourg, Neuchâtel",
"canton": null,
"employmentType": "Permanent position",
"workload": "100%",
"workloadMin": 100,
"workloadMax": 100,
"salaryText": "CHF 80,400 - 114,000 per year",
"salaryMin": 80400,
"salaryMax": 114000,
"salaryCurrency": "CHF",
"salaryPeriod": "YEAR",
"publishedAt": "2026-10-01T02:00:12+02:00",
"site": "jobs.ch",
"searchTerm": "python",
"page": 2,
"position": 17,
"scrapedAt": "2026-10-03T00:00:00.000Z"
}

Use it as an API

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("lukehunter/jobs-ch-scraper").call(
run_input={
"searchTerms": ["python", "data engineer"],
"location": "Zürich",
"site": "jobs.ch",
"maxItems": 200,
}
)
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
print(job["title"], "-", job["company"], "-", job["location"], job["salaryText"])

Pricing and cost control

Pay per delivered job.

$0.0015 per job — 1,000 jobs = $1.50.

Jobs deliveredCost
20$0.03
1,000$1.50
10,000$15.00

There is no charge for starting a run, and no charge for a search that is skipped, blocked or finds nothing. A job that appears on two result pages of the same search is delivered and charged once. maxItems gives you a hard upper bound on cost.

Notifications to Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover and webhooks are free: you pay only for delivered jobs, never for messages sent.

Limitations

  • Search results only. The Actor reads the search-result pages, not each job's detail page, so it does not collect the full job description, requirements or application route. url links to the ad.
  • Salary is rarely published. Most Swiss ads do not state a salary; in the sample pages used to build this Actor roughly 1 ad in 20 did. Those fields are null otherwise. Where a salary is published it is passed on exactly as the advertiser entered it - an occasional ad carries an obviously mis-entered range (for example 90 - 100 per year), and the Actor does not correct it.
  • No recruiter or contact data, by design. No phone numbers, e-mail addresses or contact persons are collected.
  • Same ad, two sites. jobs.ch and jobup.ch share many ads, but an ad published on both gets a different id on each site. Deduplicate on title + company if you search both.
  • Language. Ad titles stay in the language the employer wrote them in (German, French, English). employmentType is as the site publishes it; it is null when the ad has none.
  • Up to 200 jobs per search (10 pages x 20 jobs). Use narrower keywords or a location for more precise coverage of large result sets.
  • Blocks are reported, not bypassed. If the site serves a challenge page or an error, the Actor stops that search, says so in the status message, and keeps whatever earlier pages it already collected. You are not charged for a blocked search.
  • Results change. Ads are added and withdrawn all the time, and the site's relevance ranking decides the order.
  • This is an independent tool, not affiliated with, endorsed by or connected to jobs.ch, jobup.ch or JobCloud. You are responsible for using the data in line with the sites' terms and applicable law.

Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

One API call that runs the Actor and returns the data (replace $APIFY_TOKEN with your token from Apify Console → Settings → Integrations):

curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~jobs-ch-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"searchTerms": ["python"], "site": "jobs.ch", "maxPagesPerSearch": 1, "maxItems": 20}'

JavaScript (npm i apify-client):

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~jobs-ch-scraper').call({
"searchTerms": [
"python"
],
"site": "jobs.ch",
"maxPagesPerSearch": 1,
"maxItems": 20
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Python (pip install apify-client):

import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~jobs-ch-scraper').call(run_input={"searchTerms": ["python"], "site": "jobs.ch", "maxPagesPerSearch": 1, "maxItems": 20})
items = client.dataset(run['defaultDatasetId']).list_items().items

Schedules, webhooks and no-code tools: save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

Only new items each run: set "onlyNewSinceLastRun": true on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

Reliability: an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

Feedback: an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

FAQ

Does this collect recruiter or applicant information?

No. Only what the ad itself shows publicly - title, company, place, workload, employment type, published salary and date.

Can I search only one city or canton?

Yes - set location (for example Zürich), or paste a search URL with the location filter already applied into startUrls.

Can I search jobup.ch in French?

Set site to jobup.ch and use French keywords, or paste a https://www.jobup.ch/fr/emplois/?term=... URL into startUrls.

How do I get alerted to new jobs instead of the same ones every time?

Set onlyNewSinceLastRun: true and schedule the Actor - see "New-job alerts" above. The first run delivers everything and remembers it; later runs deliver only what is new, and you pay only for those.

Why did a run finish with fewer jobs than maxItems?

The search simply had fewer results, maxPagesPerSearch was reached, or a page was blocked or failed. The run's status message says which.

Can I get Swiss job alerts in Discord, Telegram, Slack or on my phone?

Yes. Set discordWebhookUrl, telegramBotToken + telegramChatId, slackWebhookUrl, teamsWebhookUrl or googleChatWebhookUrl, or use ntfyTopic (free, no account) or pushoverUserKey + pushoverAppToken for phone pushes. Mattermost and Rocket.Chat work through slackCompatibleWebhookUrl. With onlyNewSinceLastRun on and a schedule, each run sends only the jobs it has not delivered before. Setup steps for each are in the alerts section above. Notifications cost nothing extra.

Can I get Swiss job alerts by email?

Not natively. There are two routes. (1) Set webhookUrl to a Zapier, Make or n8n catch hook and add an email or Gmail step: the webhook delivers every row as JSON, so the email can list the jobs. (2) Apify's own run integrations and run notifications can email you when a run finishes, but they only send a link to the run or dataset, not the jobs themselves. For instant alerts without building anything, Telegram, ntfy or Pushover reach your phone directly.

Can I get Swiss job alerts in WhatsApp?

Not natively, and we do not use unofficial WhatsApp APIs, which risk getting your number banned. The official route is the WhatsApp Business Platform (Cloud API) with its own business account, message templates and pricing: point webhookUrl at a Zapier, Make or n8n flow that calls it. For free phone alerts use Telegram or ntfy instead.

More from this publisher