Quick answer
Arbeitnow's job-board API accepts a remote=true query parameter, echoes it right back in the pagination links.next URL on every page, and then ignores it completely when building the response. We pulled page 1 twice in the same minute — once plain, once with ?remote=true tacked on — and got the same 250 jobs in the same order both times, of which only 12 actually had remote: true. If your client trusts the URL it's told to follow next, it will happily paginate through an entire "filtered" feed that was never filtered at all.
The parameter that lied twice 🔎
Here's the exact comparison, curl against the live endpoint minutes apart:
curl -s "https://www.arbeitnow.com/api/job-board-api?page=1" -o plain.json
curl -s "https://www.arbeitnow.com/api/job-board-api?page=1&remote=true" -o filtered.json
plain.json slugs (250) == filtered.json slugs (250) -> True
filtered.json jobs actually remote: true -> 12 / 250
filtered.json "links": {"next": ".../job-board-api?remote=true&page=2"}
That last line is the trap. The API takes your query string, folds it into the links.next field of its own pagination envelope, and hands it back like a receipt confirming the filter is live. It isn't. Nothing about the response other than that one string suggests the parameter did anything, and if you write a paginator that just follows links.next blindly — which is the normal, correct thing to do with a Laravel-style paginated API — you will walk the entire unfiltered feed, one remote=true-branded page at a time, and never notice.
The second thing: the same job returns two different HTML strings 🎭
We also compared the description field of every job that appeared in both pulls, by slug. 113 of 250 (45%) had a byte-different description_html on the second call than the first — same job, same slug, same title, called seconds apart.
The diff is always in the same place: a footer Arbeitnow appends to the description HTML, alternating between two backlink variants:
A: <p>Find <a href="https://www.arbeitnow.com">Jobs in Germany</a> on Arbeitnow</p>
B: <p>Find more <a href="https://www.arbeitnow.com/english-speaking-jobs">English Speaking Jobs in Germany</a> on Arbeitnow</p>
Arbeitnow's own API terms ask API consumers to "appreciate linking back to the site" — this looks like the server doing exactly that for itself, injecting a rotating self-promotional link into every description at serve time. Fair enough. But it means description_html is not a stable value for a given slug. If you hash the description to detect "did this posting change" between two scheduled runs, roughly half your postings will register a false change every single time, for a reason that has nothing to do with the actual job content.
What this means for a client you'd write yourself
Two failure modes, both invisible until you check the wire:
-
Trusting the pagination envelope over the data. A
links.nextURL that carries your filter forward is not proof the filter was applied upstream — only counting the actual field values in the payload is. We never send query parameters this API is known to ignore; every filter (remoteOnly,tags,locationContains) runs client-side against the fetched page, so what you configure is what you get regardless of what the server claims to support. -
Hashing a field for change detection that the source rewrites on every call. A content hash needs a source that's actually content-stable. On this feed,
description_htmlisn't one field you can trust for that — which is a small, specific thing to know before you build a "notify me when this posting changes" pipeline on top of it.
Fault isolation matters here too: a single job with a shape the parser doesn't expect is logged and skipped, and the rest of that page's postings still land. A 250-row page shouldn't cost you the whole run because of one entry.
Output
One row per job posting, JSON/CSV/Excel:
slug, title, company_name, description_html, remote, url,
tags[], job_types[], location, posted_at, page
from apify_client import ApifyClient
client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("DevilScrapes/arbeitnow-jobs-scraper").call(
run_input={"maxResults": 50, "maxPages": 2, "remoteOnly": True, "tags": ["Engineering"]}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["title"], "—", item["company_name"])
Pricing is $0.20 to start a run plus $0.0015 per job row — $1.70 for a thousand postings. A run that finds nothing within maxPages still succeeds, and costs only the start fee.
→ Arbeitnow Jobs Scraper on Apify
Built by Devil Scrapes. We rotate Chrome/Firefox TLS fingerprints and retry with backoff on every request, and we never trust a query parameter — or the pagination link it comes back wrapped in — until we've checked the actual field values against it.
Top comments (0)