Collect a company's publicly listed jobs from its Revolut People careers board ({company-slug}, for example ngcash) without logging in, submitting applications, using a paid proxy, or modifying site state. Verify selected detail links by loading their opaque-ID position pages and extracting visible job details. When available, use same-origin public postings APIs for lower-latency read-only metadata retrieval.
Use Cases
Use for requests to enumerate a company's public vacancies or read and verify one or more listed positions. Inputs may include no filter, a title keyword such as {query}, or a known position URL. Preserve all positions by default; apply title filtering only when requested.
Automation Flow
- Navigate directly to
https://revolutpeople.com/{company-slug}/public/careers/with a Windows browser persona and wait fordomcontentloaded. - If Cloudflare presents an interstitial, run Browserless's Cloudflare solver and wait for the careers page. Do not reuse or expose the transient
__cf_chl_rt_tkquery parameter. If a post-challenge consent/control button blocks the board, dismiss only that control, then continue read-only. In the observed deployment,button[type='button']:nth-of-type(2)was the blocking control, but confirm its visible role before clicking because positional selectors are fragile. - In one page evaluation, enumerate every
a[aria-label="position-link"], resolve each opaque UUID from its URL, and optionally filter titles locally by{query}using case-insensitive matching. Leave the collection unfiltered when no query is supplied. - For selected UUIDs, first use the optional same-origin API route
/api/{company-slug}/external/v2/postings/{position-id}. This avoids loading each detail page and returns the API's complete public posting object when available. Never invent or derive a UUID from a title. - If the v2 response is unavailable, incomplete, or rendered-page verification is requested, navigate directly to each selected absolute position URL. A loaded page with a non-empty
h1and visiblemainor body content verifies the link; then run the detail extractor. Preserve board order and return one record per verified link. - As an additional enumeration path, inspect
/api/{company-slug}/external/v3/postings?page=1and/api/{company-slug}/external/v3/postings/statsfrom the careers page. Use the v3 postings response only if it contains the complete public posting collection in the current deployment; otherwise use the DOM board extractor. Do not construct a position URL without first reading its opaque ID from the board or a confirmed API response.
Use this board extractor on the loaded careers page:
(() => [...document.querySelectorAll('a[aria-label="position-link"]')]
.map(a => ({
title: (a.innerText || '').trim(),
href: a.href,
isOpaquePositionUrl: (() => {
try {
const u = new URL(a.href, location.href);
return /\/[^/]+\/public\/careers\/position\/[^/]+-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\/?$/i.test(u.pathname);
} catch (_) { return false; }
})()
}))
.filter(x => x.title && x.href))()For optional title filtering and the v2 API shortcut, run this on the loaded careers page. Set {query} to an empty string for no filter:
(async () => {
const query = '{query}'.trim().toLowerCase();
const companySlug = '{company-slug}';
const links = [...document.querySelectorAll('a[aria-label="position-link"]')]
.map(a => ({ title: (a.innerText || '').trim(), href: a.href }))
.filter(x => x.title && x.href && (!query || x.title.toLowerCase().includes(query)));
const rows = [];
for (const link of links) {
const match = new URL(link.href, location.href).pathname.match(/\/position\/[^/]*-([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/?$/i);
if (!match) continue;
const id = match[1];
let status = null, data = null, usable = false;
try {
const response = await fetch(`/api/${companySlug}/external/v2/postings/${id}`, { headers: { accept: 'application/json' } });
status = response.status;
const bodyText = await response.text();
if (response.ok) {
try {
const j = JSON.parse(bodyText);
// Schema-agnostic completeness: a real posting carries several fields and
// a substantive text body (title/description-like). A stub or error object
// such as {} or {"message":"blocked"} fails this and falls back to the page.
const vals = j && typeof j === 'object' && !Array.isArray(j) ? Object.values(j) : [];
const textLen = vals.filter(v => typeof v === 'string').join(' ').trim().length;
if (vals.length >= 3 && textLen >= 40) { data = j; usable = true; }
} catch (_) {}
}
} catch (_) { /* network error — fall through to page-extraction fallback */ }
// Usable API payload, else fall back to rendered-page extraction per step 5; never drop the posting.
rows.push(usable
? { title: link.title, href: link.href, id, status, source: 'api', data }
: { title: link.title, href: link.href, id, status, source: 'needs-page-extraction', data: null });
}
return rows;
})()Use this detail extractor on each loaded position page:
(() => {
const text = el => (el?.innerText || '').trim();
const main = document.querySelector('main') || document.body;
const content = text(main).slice(0, 12000);
return {
url: location.href,
title: text(document.querySelector('h1')),
pageTitle: document.title,
verified: Boolean(document.querySelector('h1')) && Boolean(content),
content
};
})()Possible Friction Points
- The public board can be protected by a Cloudflare interstitial; a Windows browser persona plus the Browserless Cloudflare solver was required in the observed run.
- Challenge URLs may contain a transient
__cf_chl_rt_tktoken. Treat it as session-specific and construct requests from the clean careers URL instead. - After challenge completion, a consent or control button may block the board. The observed run dismissed
button[type='button']:nth-of-type(2); verify the button's visible purpose before using this fragile positional selector. - Position links use an opaque UUID embedded after a human-readable slug, for example
/{company-slug}/public/careers/position/{slug}-{uuid}. Resolve links froma[aria-label="position-link"]or a confirmed API response before opening details; never fabricate an ID from a title. - The public same-origin API exposes v2 detail routes in the form
/api/{company-slug}/external/v2/postings/{uuid}. The UUID must come from a current board link or confirmed API response. Returned fields can vary, so preserve the response object rather than relying on a fixed schema. - The public same-origin API also uses
/api/{company-slug}/external/v3/postings?page=1and/api/{company-slug}/external/v3/postings/stats; the postings endpoint is an optional enumeration shortcut and should be checked for completeness before replacing the DOM collection. - The observed board link selector is specific and preferable to scraping all anchors. The detail page has a stable
h1andmaintext container, but themainfallback tobodyis retained for layout variations. - Keep all navigation and API requests read-only: do not click application controls beyond a blocking consent/control dismissal, submit forms, authenticate, or use a paid proxy.