All platforms

Jobvite Jobs API.

Pull every open req from a company's server-rendered Jobvite board and enrich each one with the JSON-LD JobPosting data embedded in its detail page — no official jobs API required.

Get API access
Jobvite
Live
150K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Jobvite
NutanixEnphase EnergyZiff DavisRackspaceFirstCash
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 Jobvite.

Data fields
  • Full Job Descriptions
  • Structured Locations
  • Requisition Numbers
  • Department & Category
  • Salary & Employment Type
  • Posted & Expiry Dates
Use cases
  1. 01Enterprise Job Monitoring
  2. 02Competitive Talent Intelligence
  3. 03Career Page Aggregation
  4. 04Salary Benchmarking
Trusted by
NutanixEnphase EnergyZiff DavisRackspaceFirstCash
DIY GUIDE

How to scrape Jobvite.

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

HTMLintermediateNo published limit; space requests ~500ms apart and cap detail fetches at ~3 concurrentNo auth

Fetch the company job board page

Request the company's board at jobs.jobvite.com/{company}. Jobvite intermittently serves a 'Page Unavailable' placeholder, so retry a few times with a short delay before treating the body as real.

Step 1: Fetch the company job board page
import time
import requests

# Markers Jobvite emits when the board is briefly warming up
TRANSIENT_MARKERS = (
    "Page Unavailable",
    "Job listings are currently unavailable but they will return shortly",
)

def is_transient(html: str) -> bool:
    lower = html.lower()
    return all(marker.lower() in lower for marker in TRANSIENT_MARKERS)

def fetch_jobvite_page(url: str, max_attempts: int = 3, delay: float = 0.5) -> str:
    html = ""
    for attempt in range(1, max_attempts + 1):
        response = requests.get(url, timeout=15)
        response.raise_for_status()
        html = response.text
        if not is_transient(html) or attempt == max_attempts:
            return html
        time.sleep(delay)  # back off, then retry
    return html

company = "nutanix"
board_url = f"https://jobs.jobvite.com/{company}"
html = fetch_jobvite_page(board_url)
print(f"Loaded {len(html)} bytes")

Parse job rows from the listings table

Jobvite renders listings as table rows. Read the anchor inside each <td class="jv-job-list-name"> cell and the sibling <td class="jv-job-list-location"> cell. Job IDs are mixed-case, variable-length strings, so match /job/([A-Za-z0-9]+) rather than a fixed pattern.

Step 2: Parse job rows from the listings table
import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup

def extract_job_id(url: str) -> str | None:
    match = re.search(r"/job/([A-Za-z0-9]+)(?:[?/]|$)", url)
    return match.group(1) if match else None

def extract_job_rows(html: str, base_url: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    jobs = []

    for name_cell in soup.select("td.jv-job-list-name"):
        link = name_cell.find("a", href=True)
        if not link:
            continue
        job_id = extract_job_id(link["href"])
        if not job_id:
            continue

        row = name_cell.find_parent("tr")
        location_cell = row.find("td", class_="jv-job-list-location") if row else None
        jobs.append({
            "id": job_id,
            "title": link.get_text(strip=True),
            "location": location_cell.get_text(strip=True) if location_cell else None,
            "url": urljoin(base_url + "/", link["href"]),
        })

    # Fallback: scan for any /job/<id> anchor when the table layout is absent
    if not jobs:
        seen = set()
        for anchor in soup.find_all("a", href=re.compile(r"/job/[A-Za-z0-9]+")):
            job_id = extract_job_id(anchor["href"])
            if not job_id or job_id in seen:
                continue
            seen.add(job_id)
            jobs.append({
                "id": job_id,
                "title": anchor.get_text(strip=True) or "Untitled",
                "url": urljoin(base_url + "/", anchor["href"]),
            })

    return jobs

jobs = extract_job_rows(html, board_url)
print(f"Found {len(jobs)} jobs")

Paginate through the full board

Additional pages are the same board URL with ?p={page} appended (page 1 is the bare URL). Stop when the 'start-end of total' summary shows you have reached the end, or when a page returns no new rows.

Step 3: Paginate through the full board
def get_all_jobs(company: str) -> list[dict]:
    base_url = f"https://jobs.jobvite.com/{company}"
    all_jobs = []
    page = 1

    while True:
        page_url = base_url if page <= 1 else f"{base_url}?p={page}"
        html = fetch_jobvite_page(page_url)
        rows = extract_job_rows(html, base_url)
        if not rows:
            break

        all_jobs.extend(rows)

        # "1-50 of 229" style summary tells us when to stop
        text = BeautifulSoup(html, "html.parser").get_text(" ")
        summary = re.search(r"([0-9]+)\s*[-–]\s*([0-9]+)\s+of\s+([0-9]+)", text)
        if summary:
            if int(summary.group(2)) >= int(summary.group(3)):
                break
        elif not re.search(rf"[?&]p={page + 1}(?=[^0-9]|$)", html):
            break

        page += 1
        time.sleep(0.5)  # be respectful

    return all_jobs

Extract job details from JSON-LD and HTML

Each detail page embeds a JSON-LD JobPosting block — prefer it for the description, dates, salary, employment type, and locations. Fall back to the .jv-header / .jv-job-detail-meta / .jv-job-detail-description classes, and pull the apply link from the first anchor mentioning 'apply'.

Step 4: Extract job details from JSON-LD and HTML
import json

def find_jsonld_jobposting(soup: BeautifulSoup) -> dict | None:
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or tag.get_text())
        except (json.JSONDecodeError, TypeError):
            continue
        for item in (data if isinstance(data, list) else [data]):
            if isinstance(item, dict) and item.get("@type") == "JobPosting":
                return item
    return None

def parse_meta(meta_text: str) -> tuple:
    # Meta is pipe-delimited, e.g. "Engineering | Bangalore, India | Req.Num.: 12345"
    department = location = req_number = None
    for raw in (meta_text or "").split("|"):
        part = raw.strip()
        if not part:
            continue
        if re.match(r"^req[. ]", part, re.IGNORECASE):
            req_number = re.sub(r"^req[. ]*(num[. ]*:?)?[. :]*", "", part, flags=re.IGNORECASE).strip()
        elif "," in part:
            location = part
        elif department is None:
            department = part
    return department, location, req_number

def jsonld_locations(posting: dict) -> list[str]:
    raw = posting.get("jobLocation")
    entries = raw if isinstance(raw, list) else [raw] if raw else []
    out = []
    for loc in entries:
        address = (loc or {}).get("address", {})
        parts = [address.get("addressLocality"), address.get("addressRegion"), address.get("addressCountry")]
        parts = [p for p in parts if p]
        if parts:
            out.append(", ".join(parts))
    return out

def get_job_details(company: str, job_id: str) -> dict:
    url = f"https://jobs.jobvite.com/{company}/job/{job_id}"
    html = fetch_jobvite_page(url)
    soup = BeautifulSoup(html, "html.parser")
    posting = find_jsonld_jobposting(soup) or {}

    title_el = soup.find("h2", class_="jv-header")
    desc_el = soup.find("div", class_="jv-job-detail-description")
    meta_el = soup.find("p", class_="jv-job-detail-meta")
    department, location, req_number = parse_meta(meta_el.get_text(" ", strip=True) if meta_el else "")

    job = {
        "id": job_id,
        "url": url,
        "title": posting.get("title") or (title_el.get_text(strip=True) if title_el else None),
        # decode_contents keeps nested <div> blocks intact (naive regex truncates them)
        "description": posting.get("description") or (desc_el.decode_contents() if desc_el else None),
        "department": department,
        "requisition_number": req_number,
        "posted_at": posting.get("datePosted"),
        "closes_at": posting.get("validThrough"),
        "employment_type": posting.get("employmentType"),
    }

    # Locations: JSON-LD jobLocation first, then the pipe-delimited meta
    locations = jsonld_locations(posting)
    if not locations and location:
        locations = [location]
    job["locations"] = locations

    # Apply link: first anchor whose href, text, or class mentions "apply"
    for anchor in soup.find_all("a", href=True):
        fields = (anchor["href"], anchor.get_text(), " ".join(anchor.get("class", [])))
        if any("apply" in (value or "").lower() for value in fields):
            job["apply_url"] = urljoin(url, anchor["href"])
            break
    job.setdefault("apply_url", url)

    return job

detail = get_job_details("nutanix", jobs[0]["id"])
print(detail["title"], "-", detail["locations"])

List filter options with the facets endpoint

The only JSON endpoint on Jobvite is /search/facets?nl=1. It returns filter options (regions, locations, departments, categories, job types) — not job listings — which is handy for building search UIs on top of your scraped data.

Step 5: List filter options with the facets endpoint
def get_facets(company: str, location: str | None = None) -> dict:
    url = f"https://jobs.jobvite.com/{company}/search/facets"
    params = {"nl": 1}
    if location:
        params["l"] = location  # e.g. "Bangalore, India"

    response = requests.get(url, params=params, timeout=15)
    response.raise_for_status()
    return response.json().get("facets", {})

facets = get_facets("nutanix")
print("Locations:", len(facets.get("locations", [])))
print("Categories:", len(facets.get("categories", [])))
print("Departments:", len(facets.get("departments", [])))
Common issues
criticalNo public jobs API — only server-rendered HTML

Jobvite exposes no JSON endpoint for listings; job data lives in server-rendered HTML. Parse the board page for links and read each detail page. The only working JSON route, /search/facets, returns filter options, not jobs.

highJob IDs are mixed-case and variable length

IDs like 'oXYZabc123' are not lowercase 9-character strings. Match /job/([A-Za-z0-9]+) — a fixed lowercase pattern silently drops jobs whose IDs contain uppercase letters or a different length.

mediumIntermittent 'Page Unavailable' placeholder

Jobvite sometimes returns a page containing 'Job listings are currently unavailable but they will return shortly'. Detect that marker and retry (up to 3 times, ~500ms apart) before treating the board as empty.

mediumNested divs truncate naive description regex

Some tenants (e.g. FirstCash) nest a <div class="jv-meta"> inside the description div, which cuts a greedy regex short. Parse with an HTML parser like BeautifulSoup, or prefer the JSON-LD 'description' field.

mediumOnly jobs.jobvite.com boards are recognized

Listing URLs are validated against the jobs.jobvite.com host. Companies serving Jobvite from a custom domain won't match — detect the host up front and handle those boards separately.

lowLocations missing on some tenants

A few boards (e.g. Ziff Davis) omit locations from the listing and meta HTML. Fall back to the JSON-LD jobLocation address, and tolerate an empty location list rather than failing the record.

Best practices
  1. 1Prefer the embedded JSON-LD JobPosting block for description, salary, and dates; fall back to the .jv-* HTML classes.
  2. 2Match job IDs with /job/([A-Za-z0-9]+) — never assume a fixed length or lowercase-only pattern.
  3. 3Retry the 'Page Unavailable' placeholder up to 3 times with a ~500ms delay before giving up.
  4. 4Deduplicate listings by job ID, since featured jobs are repeated at the top of the board.
  5. 5Space requests ~500ms apart and cap concurrent detail fetches at ~3 to stay within the board's tolerance.
  6. 6Parse detail pages with BeautifulSoup, not raw regex, so nested description divs aren't truncated.
Or skip the complexity

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

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

Access Jobvite
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