All platforms

Avature Jobs API.

Pull structured vacancies from the hosted career sites that large global enterprises run on their own *.avature.net subdomains — full descriptions, locations, and posting dates included.

Get API access
Avature
Live
<3haverage discovery time
1hrefresh interval
Companies using Avature
MaximusMetro BankDeloitte
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.

What's in every response.

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

Data fields
  • Full Job Descriptions
  • Structured Job Locations
  • Posting & Closing Dates
  • Employment Type
  • Hiring Organization Name
  • Native Apply Links
Use cases
  1. 01Enterprise Hiring Intelligence
  2. 02Global Talent Market Tracking
  3. 03Career-Site Job Aggregation
  4. 04Competitor Vacancy Monitoring
Trusted by
MaximusMetro BankDeloitte
DIY GUIDE

How to scrape Avature.

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

HTMLadvanced~3 concurrent detail requests, ~500ms between requests (self-imposed)No auth

Build the SearchJobs URL from the tenant and portal

An Avature board is identified by the account subdomain (the left-most host label) plus a tenant-configurable portal/locale path. Preserve both — one Avature account can host several independent portals — and request the SearchJobs collection page.

Step 1: Build the SearchJobs URL from the tenant and portal
import requests

tenant = "maximus"          # left-most label of {tenant}.avature.net
portal = "careers"          # path segment(s) before /SearchJobs/, kept per tenant
base_url = f"https://{tenant}.avature.net/{portal}"
search_url = f"{base_url}/SearchJobs/"

headers = {"User-Agent": "Mozilla/5.0 (compatible; JobBot/1.0)"}
html = requests.get(search_url, headers=headers, timeout=30).text

Extract canonical detail links and the result total

Listings are anchors pointing at /FolderDetail/{slug}/{id} or /JobDetail/{slug}/{id}; the trailing number is the native job ID. Read the advertised 'N results' total and detect the portal mode, which decides the pagination parameter.

Step 2: Extract canonical detail links and the result total
import re
from bs4 import BeautifulSoup

DETAIL_RE = re.compile(r"/(FolderDetail|JobDetail)/[^/?#]+/(\d+)(?:[/?#]|$)", re.I)

def parse_listings(page_html, page_url):
    soup = BeautifulSoup(page_html, "html.parser")
    jobs, order = {}, []
    for a in soup.select("a[href]"):
        absolute = requests.compat.urljoin(page_url, a.get("href", ""))
        m = DETAIL_RE.search(absolute)
        if not m:
            continue
        job_id = m.group(2)
        title = (a.get("title") or a.get_text()).strip()
        if job_id in jobs:
            if not jobs[job_id]["title"] and title:  # image + text anchor pair
                jobs[job_id]["title"] = title
            continue
        order.append(job_id)
        jobs[job_id] = {"id": job_id, "title": title, "url": absolute.split("?")[0]}
    return [jobs[i] for i in order]

def results_total(page_html):
    m = re.search(r"(\d[\d,]*)\s+results?\b", page_html, re.I)
    return int(m.group(1).replace(",", "")) if m else None

mode = "folder" if "/FolderDetail/" in html else "job"
offset_param = "folderOffset" if mode == "folder" else "jobOffset"

Page through the offset navigation

Avature ignores any *RecordsPerPage override and serves a fixed page size per tenant (~6–10). Advance the offset by the first page's job count and stop once the deduplicated count reaches the advertised total or a page returns no new IDs. Keep a hard page cap as a safety net.

Step 3: Page through the offset navigation
first = parse_listings(html, search_url)
page_size = len(first) or 6
total = results_total(html)

seen = {j["id"] for j in first}
all_jobs = list(first)
offset, MAX_PAGES = page_size, 500

for _ in range(1, MAX_PAGES):
    if total is not None and len(seen) >= total:
        break
    page_url = f"{search_url}?{offset_param}={offset}"
    page_html = requests.get(page_url, headers=headers, timeout=30).text
    added = 0
    for job in parse_listings(page_html, page_url):
        if job["id"] not in seen:
            seen.add(job["id"])
            all_jobs.append(job)
            added += 1
    if added == 0:            # no new IDs -> pagination exhausted
        break
    offset += page_size

print(f"Collected {len(all_jobs)} of {total or len(all_jobs)} jobs")

Read the JobPosting JSON-LD from each detail page

The canonical detail document embeds a schema.org JobPosting block with the title, description, dates, employment type, hiring organization, and locations. Job-mode tenants entity-encode the HTML description, so unescape it before stripping tags.

Step 4: Read the JobPosting JSON-LD from each detail page
import json, html as html_lib

def fetch_detail(url):
    page = requests.get(url, headers=headers, timeout=30).text
    soup = BeautifulSoup(page, "html.parser")

    posting = None
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except (json.JSONDecodeError, TypeError):
            continue
        for node in (data if isinstance(data, list) else [data]):
            if isinstance(node, dict) and node.get("@type") == "JobPosting":
                posting = node
                break
        if posting:
            break

    job = {"url": url}
    if posting:
        job["title"] = posting.get("title")
        desc = posting.get("description")
        if desc:
            job["description"] = BeautifulSoup(
                html_lib.unescape(desc), "html.parser").get_text(" ", strip=True)
        job["posted_at"] = posting.get("datePosted")
        job["closes_at"] = posting.get("validThrough")
        job["employment_type"] = posting.get("employmentType")
        org = posting.get("hiringOrganization")
        job["company"] = org.get("name") if isinstance(org, dict) else None
    return job, soup

Fall back to labelled HTML and find the apply route

Some tenants omit the JSON-LD and expose provider-owned fields in .article__content__view__field blocks plus a separate description article. Require a substantive description (80+ chars) before trusting a page, and resolve the native apply link: ApplicationMethods in folder mode, RegisterMethod in job mode.

Step 5: Fall back to labelled HTML and find the apply route
def enrich_from_html(soup, job):
    fields = {}
    for field in soup.select(".article__content__view__field"):
        label = field.select_one(".article__content__view__field__label")
        value = field.select_one(".article__content__view__field__value")
        if label and value:
            fields.setdefault(label.get_text(strip=True), value.get_text(" ", strip=True))

    job["company"] = job.get("company") or fields.get("Company")
    job["location"] = (fields.get("Primary Location")
                       or fields.get("Job Location") or fields.get("Location"))
    job["employment_type"] = (job.get("employment_type")
                              or fields.get("Employment Type") or fields.get("Contract Type"))

    if not (job.get("description") and len(job["description"]) >= 80):
        for article in soup.select(".article.article--details"):
            head = (article.select_one(".article__header__text__title") or article)
            heading = re.sub(r"[^a-z0-9]", "", head.get_text().lower())
            if "description" in heading or heading in {"abouttherole", "therole", "roleoverview"}:
                body = article.select_one(".article__content, .article__body")
                text = body.get_text(" ", strip=True) if body else ""
                if len(text) >= 80:
                    job["description"] = text
                    break

    apply = soup.select_one("a[href*='/ApplicationMethods'], a[href*='/RegisterMethod']")
    if apply:
        job["apply_url"] = requests.compat.urljoin(job["url"], apply.get("href"))
    return job
Common issues
highWrong pagination parameter returns the same first page forever

Avature runs two portal modes. Detect '/FolderDetail/' in the SearchJobs HTML and paginate with folderOffset; otherwise use jobOffset. Advance the offset by the first page's job count, not by a guessed page size — the *RecordsPerPage override is ignored.

highDetail page has no JobPosting JSON-LD, or its description is entity-encoded

Fall back to the labelled .article__content__view__field blocks and the description article whose heading contains 'description' / 'about the role'. When JSON-LD is present, HTML-unescape the description before stripping tags. Reject pages whose description is under ~80 characters as likely blocked or thin.

mediumNon-job routes leak into the listing links

Match only canonical /FolderDetail/{slug}/{id} and /JobDetail/{slug}/{id} paths. Skip Login, RegisterInterest, ApplicationMethods, RegisterMethod, portal and asset (ASSET/assetFile) routes — they are candidate/apply/static pages, not vacancies. Ignore social-share links that merely embed a detail URL inside a query string.

mediumCustom mirror hosts and volatile query tokens break URL matching

Some tenants publish detail pages on a branded mirror (for example emea.workmyway.com) rather than the .avature.net host. Preserve the public host exactly and only drop the volatile ?qtvc= token when normalizing the detail URL.

mediumRequests get 401, 403, or 429 responses

Treat 401 as auth-required and 403/429 as a soft block: throttle to a few concurrent detail fetches with ~500ms between requests and back off on 429. A 404/410 on a detail page means the vacancy was removed.

Best practices
  1. 1Keep the tenant subdomain and the full portal/locale path together — one account can host several independent job universes.
  2. 2Detect the portal mode from the link verb: /FolderDetail/ paginates with folderOffset, /JobDetail/ with jobOffset.
  3. 3Advance the offset by the first page's job count and stop when the deduplicated count meets the advertised 'N results' total.
  4. 4Prefer the JobPosting JSON-LD block, then fall back to labelled .article__content fields for description, location, and employment type.
  5. 5Exclude Login, RegisterInterest, ApplicationMethods, RegisterMethod, and asset paths from listing discovery.
  6. 6Throttle to ~3 concurrent detail requests with ~500ms spacing to stay under the block threshold.
Or skip the complexity

One endpoint. All Avature jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=avature" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access Avature
job data today.

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

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