Devex Jobs Scraper — Complete Source Descriptions
Pricing
from $3.00 / 1,000 job results
Devex Jobs Scraper — Complete Source Descriptions
Discover public Devex jobs through Camoufox and Apify Residential. Emits exact canonical narrative text proven either by a top-level structured-metadata boundary or by the end of a selector node with no recognized metadata child or standalone metadata-label line.
Discover public Devex job postings through Camoufox and Apify Residential and return only records with the exact canonical source-page narrative. The Actor first uses public-search metadata only to find canonical candidate URLs; it never emits that metadata as a job. It opens the Residential Devex listing only if verified detail pages from that first tier cannot fill the requested count. No model or user-provided API key is used for discovery or extraction.
The Actor reads unauthenticated public pages. It does not use a Devex login, paywall access, CAPTCHA bypass, or an authenticated Devex session.
Where records come from
The Actor serves each run from the cheapest source that can prove a complete body, in this order:
- The durable Devex inventory. A private owner-only collector
(
devex-private-collector) discovers and reads Devex on a schedule and stores boundary-proven records. When that inventory is reachable and fresh, this Actor delivers from it: no browser launch, no Residential bandwidth, and a complete description on the first try. Cached rows carrydeliveredFrom: "inventory". - The live source. When the inventory is unconfigured, unreachable, stale, or has nothing matching your filters, the Actor falls back to the Camoufox + Residential path described below. Nothing about the filters or the body contract changes; only the transport does.
An alive check (jobUrls) always uses the live path. A cached record proves
the page existed when it was collected, not that the posting is open now.
Cached rows carry descriptionContractVersion: "devex-complete-source-v4".
v4 guarantees exactly what v3 does — a complete, untruncated body that ends
where the source narrative ends — and adds one further proof mechanism:
jsonld-description, where Devex itself declares the whole narrative in
JSON-LD, so its own end is the boundary. On that boundary
descriptionMetadataChildFound and descriptionMetadataLabelLineFound are
null, because the DOM metadata-child question does not apply. Live rows keep
emitting v3 and are still held to the v3 rules exactly.
Posted dates and their provenance
Devex publishes a posting date on some jobs and none at all on others. Every
row now says which case it is, in postedAtProvenance:
| Value | postedAt means |
|---|---|
source_metadata | The Devex detail page published this date. |
listing_card | The Devex listing card published it; the detail page did not. |
inventory_admission | Devex published no date. This is the hour the durable inventory first collected the job — a collection timestamp, not a posting date. |
No date is ever invented, and relative phrases such as "3 days ago" are never
converted into one. Filter on postedWithin knowing that an
inventory_admission value is a lower bound on the real posting date.
Complete-description contract
A discovery record is emitted only when all of these checks pass:
descriptionis exact live[data-partial="job-description"]innerTextunder one of two source-owned proofs: when a recognized top-level metadata child exists, it is the exact prefix ending immediately before that rendered label; when none exists, it is the entire selector-nodeinnerText.- A recognized metadata child that cannot be located as a rendered line rejects the page. It never falls back to selector-node-end.
- The section contains at least 500 characters and begins
Job Description. descriptionContractVersionisdevex-complete-source-v3.descriptionSourceUrlnormalizes to the same canonical URL asurl.descriptionCompleteis exactlytrue.descriptionBoundaryFoundis exactlytrueand the proof fields agree:top-level-source-metadatauses a recognized label plusdescriptionMetadataChildFound=trueanddescriptionMetadataLabelLineFound=true;selector-node-enduses the stableEND_OF_SELECTOR_NODElabel with both booleansfalse. A selector-end body containing a standalone recognized metadata label is independently rejected by the caller even if its evidence fields claim otherwise.descriptionTailexactly matches the final 80 source characters.descriptionHashis the SHA-256 of the exact description string.- The description is not the listing-card snippet, does not end in an isolated
dangling word fragment, and has no ellipsis,
truncated,read more, or equivalent clipping marker at its end. - HTTP status/redirect, challenge title/body, Career Account paywall, or a missing source selector rejects the detail page.
FindJobs independently repeats these checks before mapping a record into a job. A failed contract becomes a source error, not a healthy empty result.
Input
{"keyword": "migration policy","location": "remote","postedWithin": "7d","maxItems": 3,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
postedWithin uses the shared job-search values 1h, 24h, 7d, 30d, and
any; the default is 7d. A selected window excludes records without a source-proven
posting date; any leaves unknown dates eligible. Devex currently exposes
calendar-date precision, so 24h includes the current and previous UTC date
and 1h is fail-closed instead of claiming hour-level freshness it cannot
prove.
maxItems is caller-controlled from 1 to 10 (default: 3). Keyword and location
are conservative filters over visible source listing-card text. The Actor first
uses public search metadata to discover canonical candidate URLs without a
Residential browser request, then reads every returned description directly from
Devex. It falls back to Residential listing discovery only when that cheap path
cannot fill the requested number of complete records. Supplying jobUrls switches to alive-check;
only exact numeric-id detail URLs are accepted, while query and subpath URLs
are rejected before navigation.
The first tier avoids a Residential listing navigation. It emits no job and takes no Actor start/result charge by itself, but it can issue up to three public-search requests. It is a lower-cost Actor path, not a guarantee about any third-party search service's pricing or latency.
Alive status is independent of description completeness. An explicit visible
Devex closed/expired message yields false even if an apply-looking control is
also present. An independently visible enabled apply control yields true.
When neither signal exists, isActive is null rather than guessed from page
existence.
Output
Every row follows nomad-agent-job-row-v1, the one shape shared by all of
this fleet's job Actors. A row carries every field; null means the
source did not publish that fact, and locations: [] means no
usable location was parsed from the posting. Nothing is guessed.
{"schemaVersion": "nomad-agent-job-row-v1","recordType": "posting","source": "devex","id": "a1b2c3","url": "https://example.com/devex/jobs/a1b2c3","title": "Senior Backend Engineer","company": "Example Company","locations": ["Bilbao","Spain"],"postedAt": "2026-09-02T00:00:00Z","deadline": "2026-10-15","description": "The complete posting body as plain text, exactly as the source published it — never truncated.","descriptionHtml": "<p>The complete posting body as the source's own markup.</p>","workType": "remote","custom": {"schemaId": "nomad-agent-job-custom-devex-v1","data": {"externalId": "…","descriptionContractVersion": "…","descriptionSourceUrl": "…"}}}
| Field | Meaning |
|---|---|
schemaVersion | Always "nomad-agent-job-row-v1". |
recordType | "posting" for a job, "diagnostic" for a row reporting something about the run itself. |
source | Which job source the posting came from, from the collector registry's vocabulary — not the Actor name. |
id | Stable identifier for the posting within source. |
url | Canonical public URL of the posting on the source site. |
title | Job title exactly as the source publishes it, untruncated. |
company | Employer name as published. |
locations | Places the role is based, most specific first — e.g. ["Bilbao", "Spain"]. |
postedAt | When the source published the posting, ISO-8601 UTC (YYYY-MM-DDTHH:MM:SSZ). |
deadline | Closing date for applications as an ISO-8601 calendar date (YYYY-MM-DD). |
description | The complete posting body as plain text — never truncated, never summarised. |
descriptionHtml | The posting body as the source's own markup, preserving lists, headings and links. |
workType | Working arrangement: "remote", "hybrid" or "onsite". |
custom | Facts only this source publishes, as {"schemaId", "data"}. |
A run also emits diagnostic rows — recordType: "diagnostic" with a
warnings array — when it has something to report, such as a source
returning nothing. They are never billed and are easy to filter out on
recordType.
Diagnostics and charging
Unavailable Residential access exits before the Actor-start charge. Listing or
detail transport failures emit a warning row with warning and docs. Job records are charged only
after they pass the complete-description contract and are pushed. Callers must
treat warning-only datasets as source failures rather than empty inventory.
Transport diagnostics are bounded and non-sensitive: they record whether an HTTP response existed, its status, the canonical/raw final-URL class, safe Devex host/path/query flags, and proxy scheme/hostname/port/username class plus password presence. Proxy passwords, session identifiers, and query contents are never logged or emitted.
No deployment, live canary, billing verification, or Store publication is implied by this repository version.

