Quick answer
UTC offsets are not integers. If you model a remote job's timezone requirement as list[int], you will crash or silently truncate on every role that accepts candidates in India (+5:30), Newfoundland (−3:30), Nepal (+5:45), Adelaide (+9:30) or the Marquesas (−9:30). The Himalayas remote-jobs API returns these as JSON numbers in a timezoneRestrictions array, and a live pull right now shows 37 distinct offsets, six of them fractional: -9.5, -3.5, 3.5, 4.5, 5.5, 5.75. The correct type is a float — and 5.75 is the one that catches people who "fixed" it by switching to half-hour steps.
Why does a remote job board even have fractional timezones? 🕐
Because remote hiring is expressed as an overlap window, not a location. A job that says "must overlap 4 hours with UTC+5:30" is describing India, and India has never been on a whole-hour offset. Nepal is stranger still at UTC+5:45, which is why 5.75 shows up in real data.
Here is what the field actually looks like on a live pull:
distinct offsets in one page : 37
fractional offsets present : -9.5, -3.5, 3.5, 4.5, 5.5, 5.75
Three of those are not half-hours away from anything convenient. 5.75 cannot be represented as an integer, cannot be represented as "hours plus 30 minutes", and will not round-trip through a naive int() cast — Python will happily give you 5, which is Pakistan, not Nepal. That is a silent 45-minute error in a field whose entire purpose is scheduling overlap.
So the row contract we ship is explicit about it:
timezone_restrictions: list[float] # NOT list[int]
That single annotation is the whole lesson. We shipped this Actor with list[int] in the first draft of the spec and the live data corrected us before any customer saw it.
What does this break in practice?
Three things, in rising order of how long they take to notice:
-
A hard crash, if your parser is strict.
int("5.75")raises; a Pydanticlist[int]rejects the row. This is the good outcome — it fails loudly on day one. -
A silent truncation, if your parser is permissive. You store
5for a job that means5.75, and every downstream "can this candidate overlap?" calculation is wrong by 45 minutes for that row only. - A dropped row, if your fault handling is coarse. This is the expensive one: a single un-parseable record takes down the whole batch, and the 249 good jobs on that page go with it.
That third failure mode is the one worth designing against, and it is not specific to timezones. Any job feed will eventually hand you one record shaped differently from the other 249 — a missing salary, a null company slug, an employment type nobody has seen before. If one bad record can end a run, then the reliability of your scrape is set by the worst row in the feed rather than the typical one.
How we handle it 🛡️
The Himalayas Remote Jobs Scraper isolates faults per item. A record that fails to parse is skipped and logged; the run continues and delivers everything else. You are never in the position of paying for a run that returned nothing because one job posting had an unusual field.
The other thing worth knowing about this API is that it paginates by cursor, not by page number. The response body says so itself:
Cursor pagination is now available and is the preferred way to page through the feed. Pass the
nextCursorvalue from each response back as?cursor=. It is faster than offset.
Cursor chains are also the safer choice on a feed that changes under you — new jobs are posted while you are walking it, and offset pagination on a shifting list is how you get duplicate and skipped rows. On a per-row-billed scrape, duplicates are not a cosmetic problem: they are rows a customer pays for twice.
We also do not trust a job feed's own filter parameters without checking them. Some of the documented filters on this API do not narrow the result set the way you would expect, so the Actor fetches and filters client-side. That is slightly more work per run and it means the filter you asked for is the filter you get, rather than the filter the upstream felt like applying today.
Output
One row per job posting, as JSON, CSV or Excel:
id, title, company_name, company_slug, company_logo_url, url,
excerpt, description_html, employment_type, seniority[],
categories[], parent_categories[], location_restrictions[],
timezone_restrictions[] ← float, and now you know why
min_salary, max_salary, salary_period, currency,
published_at, expires_at, scraped_at
Pricing is $0.20 to start a run plus $0.0015 per job row — $1.70 for a thousand postings. Zero rows written means you pay only the start fee.
Built by Devil Scrapes. We publish the wire-format quirks we hit, because the failure that costs you a night is rarely the one in the docs.
Top comments (0)