Connexys Recruitment Jobs API.

Connexys is a Salesforce-based recruitment suite popular with Dutch and Belgian employers. Its public vacancy search renders on Salesforce Sites, with each vacancy keyed by a native Salesforce record ID.

Get API access

What's in every response.

Data fields, real-world applications, and the companies already running on Connexys Recruitment.

Data fields

  • Full Vacancy Narratives
  • Native Salesforce Record IDs
  • Media Channel Scoping
  • Location Fields
  • Direct Apply Targets
  • Next-Page Traversal

Use cases

  1. 01Benelux Job Aggregation
  2. 02Salesforce Careers Page Ingestion
  3. 03Recruitment Marketing Analytics
  4. 04Careers Page Monitoring

Trusted by

  • Ortec
  • Acerta
  • Dunea
  • De Lijt
DIY GUIDE

How to scrape Connexys Recruitment.

Step-by-step guide to extracting jobs from Connexys Recruitment-powered career pages—endpoints, authentication, and working code.

API type
HTML
Difficulty
advanced
Rate limit
No published limit; ~300ms between requests, max 2 concurrent detail fetches
Authentication
No auth

Resolve the board and its media channel

Connexys boards are Visualforce pages on {org}.my.salesforce-sites.com: cxsrec__cxsSearch for the search and cxsrec__cxsSearchDetail for a vacancy. The mediaChannel query parameter partitions a board — the same org can publish different vacancy sets per channel, so it is part of the board identity.

Step 1: Resolve the board and its media channel
from urllib.parse import urlparse, parse_qs

SUFFIX = ".my.salesforce-sites.com"
PAGES = {"cxsrec__cxssearch", "cxsrec__cxssearchdetail"}

def parse_connexys(url: str) -> dict | None:
    parsed = urlparse(url)
    host = parsed.netloc.lower()
    if parsed.scheme != "https" or not host.endswith(SUFFIX):
        return None

    path = parsed.path
    slash = path.rfind("/")
    page = path[slash + 1:] if slash >= 0 else path.lstrip("/")
    if page.lower() not in PAGES:
        return None

    site_path = path[:slash] if slash > 0 else ""
    query = {k.lower(): v[0] for k, v in parse_qs(parsed.query).items()}
    media = query.get("mediachannel")
    board = f"https://{host}{site_path}/cxsrec__cxsSearch"
    if media:
        board = f"{board}?mediaChannel={media}"

    return {
        "org": host.split(".")[0],
        "site_path": site_path,
        "media_channel": media,
        "board_url": board,
        "position_id": query.get("id"),
    }

print(parse_connexys("https://dunea.my.salesforce-sites.com/dunearecruitment/apex/"
                     "cxsrec__cxsSearch?mediaChannel=a0o090000016dv9AAA"))

Parse the vacancy rows

Search results are table rows with the class dataRow. The anchor's href is a showPosition JavaScript call carrying two values: the 15-to-18-character Salesforce record ID and the media channel. Build the detail URL from those instead of following the href.

Step 2: Parse the vacancy rows
import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, quote

SHOW_POSITION = re.compile(
    "showPosition[(][ ]*'([A-Za-z0-9]{15,18})'[ ]*,[ ]*'([^']*)'", re.IGNORECASE)
ARTIFACTS = re.compile("cxsrec__cxsSearch|@connexys[.]com", re.IGNORECASE)

def parse_rows(html: str, page_url: str) -> list[dict]:
    if not ARTIFACTS.search(html):
        raise RuntimeError("page did not contain Connexys package artifacts")

    parsed = urlparse(page_url)
    directory = parsed.path[: parsed.path.rfind("/") + 1]
    origin = f"{parsed.scheme}://{parsed.netloc}"

    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for row in soup.select("tr.dataRow"):
        anchor = row.select_one("a[href*='showPosition']")
        if not anchor:
            continue
        match = SHOW_POSITION.search(anchor.get("href") or "")
        title = anchor.get_text(strip=True)
        if not match or not title:
            continue

        position_id, media = match.group(1), match.group(2)
        detail = f"{origin}{directory}cxsrec__cxsSearchDetail?id={quote(position_id)}"
        if media:
            detail = f"{detail}&mediaChannel={quote(media)}"

        location = row.select_one(".cxsrec__Location__c")
        rows.append({
            "id": position_id,
            "title": title,
            "media_channel": media or None,
            "location": location.get_text(strip=True) if location else None,
            "url": detail,
        })
    return rows

session = requests.Session()
board = "https://ortec.my.salesforce-sites.com/cxsrec__cxsSearch?mediaChannel=a0o2p00000Zdm21AAB"
resp = session.get(board, timeout=30)
resp.raise_for_status()
rows = parse_rows(resp.text, board)

Follow the next-page link

Pagination is a plain anchor with the class pageLink, labelled Volgende on Dutch boards and Next on English ones. Only follow links that stay on the same host and the same search page, and stop when the control is absent.

Step 3: Follow the next-page link
from urllib.parse import urljoin
import time

NEXT_LABELS = {"volgende", "next"}

def next_page_url(html: str, page_url: str) -> str | None:
    soup = BeautifulSoup(html, "html.parser")
    for anchor in soup.select("a.pageLink[href]"):
        if anchor.get_text(strip=True).lower() in NEXT_LABELS:
            candidate = urljoin(page_url, anchor["href"])
            if parse_connexys(candidate):
                return candidate
    return None

def scrape_board(session, board_url: str, max_pages: int = 20) -> list[dict]:
    url, jobs, seen = board_url, [], set()
    for _ in range(max_pages):
        resp = session.get(url, timeout=30)
        resp.raise_for_status()
        for row in parse_rows(resp.text, url):
            if row["id"] not in seen:
                seen.add(row["id"])
                jobs.append(row)

        url = next_page_url(resp.text, url)
        if not url:
            break
        time.sleep(0.3)
    return jobs

Decode the detail narrative

The vacancy text is split across four Connexys fields: company information, job description, job requirements and compensation. Their contents are stored as a JavaScript-escaped string in a preDecodedValue assignment, so unescape it before joining the sections.

Step 4: Decode the detail narrative
import codecs

ENCODED_VALUE = re.compile(
    "var[ ]+preDecodedValue[ ]*=[ ]*'((?:[^'])*)'[ ]*;", re.IGNORECASE | re.DOTALL)

NARRATIVE_FIELDS = [
    ".cxsrec__Company_information__c",
    ".cxsrec__Job_description__c",
    ".cxsrec__Job_requirements__c",
    ".cxsrec__Compensation_benefits__c",
]

def decode(value: str) -> str:
    try:
        return codecs.decode(value, "unicode_escape")
    except (UnicodeDecodeError, ValueError):
        return value

def fetch_detail(session, listing: dict) -> dict | None:
    resp = session.get(listing["url"], timeout=30)
    if resp.status_code in (404, 410):
        return None
    resp.raise_for_status()

    soup = BeautifulSoup(resp.text, "html.parser")
    sections = []
    for selector in NARRATIVE_FIELDS:
        element = soup.select_one(selector)
        if element is None:
            continue
        raw = element.decode_contents()
        match = ENCODED_VALUE.search(raw)
        text = decode(match.group(1)) if match else raw
        if len(BeautifulSoup(text, "html.parser").get_text(strip=True)) >= 30:
            sections.append(text.strip())

    heading = soup.select_one("h1.cxsPageTitle")
    apply_anchor = soup.select_one("a[href*='cxsSearchApply']")
    location = soup.select_one(".cxsrec__Location__c p, .cxsrec__Location__c")

    return {
        "id": listing["id"],
        "title": heading.get_text(strip=True) if heading else listing["title"],
        "description_html": "".join(sections),
        "location": location.get_text(strip=True) if location else listing.get("location"),
        "url": listing["url"],
        "apply_url": (urljoin(listing["url"], apply_anchor["href"])
                      if apply_anchor else listing["url"]),
    }
Common issues
highVacancy text comes back as escape sequences
Connexys stores its rich-text fields inside a JavaScript preDecodedValue assignment, so a straight innerHTML read returns escaped markup. Extract the quoted value and run a unicode-escape decode before joining the four narrative sections.
highTwo boards on the same org return different vacancies
The mediaChannel parameter partitions a Connexys board, and the same org routinely publishes several channels with intentionally different sets. Include mediaChannel in the board key and in every detail URL, or one channel's snapshot will keep expiring another's jobs.
mediumDetail links built from the href go nowhere
Row anchors call a showPosition JavaScript function rather than pointing at a URL. Read the record ID and media channel out of that call and construct cxsrec__cxsSearchDetail yourself, resolved against the same directory as the search page.
mediumAn empty board reads as a scrape failure
Dutch tenants render 'geen vacatures' and English ones 'no vacancies' instead of an empty table. Check for those phrases when no dataRow rows are found and record an empty board, rather than raising a parse error and retrying forever.
Best practices
  1. 1Treat the org plus mediaChannel pair as the board identity
  2. 2Read the record ID from the showPosition call rather than the anchor href
  3. 3Resolve detail URLs against the search page's own directory, which varies per org
  4. 4Unescape preDecodedValue before joining the four narrative fields
  5. 5Recognise the Dutch and English empty-board phrases before flagging a failure
  6. 6Throttle to ~300ms between requests with at most two concurrent detail fetches
Or skip the complexity

One endpoint. All Connexys Recruitment jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=connexys recruitment" \
  -H "X-Api-Key: YOUR_KEY"
Developer tools

Try the API.

Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.

Ready to integrate

Access Connexys Recruitment
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed