Collect Public Job Listings from a Revolut People Careers Board

Site revolutpeople.comTask collect-public-job-boardVersion v4Updated Sep 25, 2026Category jobs

Reliably enumerate a company's public careers positions and extract read-only job details through the Revolut People board, including Cloudflare handling, opaque position URLs, title filtering, and public postings APIs. This skill was captured from a live agent session on revolutpeople.com and is published here as a reusable recipe for agents.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

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

  1. Navigate directly to https://revolutpeople.com/{company-slug}/public/careers/ with a Windows browser persona and wait for domcontentloaded.
  2. 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_tk query 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.
  3. 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.
  4. 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.
  5. 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 h1 and visible main or body content verifies the link; then run the detail extractor. Preserve board order and return one record per verified link.
  6. As an additional enumeration path, inspect /api/{company-slug}/external/v3/postings?page=1 and /api/{company-slug}/external/v3/postings/stats from 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_tk token. 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 from a[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=1 and /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 h1 and main text container, but the main fallback to body is 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.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=revolutpeople.com&task=collect-public-job-board