- 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.
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.
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
- 01Enterprise Job Monitoring
- 02Competitive Talent Intelligence
- 03Career Page Aggregation
- 04Salary Benchmarking
Trusted by
- Nutanix
- Enphase Energy
- Ziff Davis
- Rackspace
- FirstCash
DIY GUIDE
How to scrape Jobvite.
Step-by-step guide to extracting jobs from Jobvite-powered career pages—endpoints, authentication, and working code.
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")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")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_jobsStep 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"])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
Best practices
- 1Prefer the embedded JSON-LD JobPosting block for description, salary, and dates; fall back to the .jv-* HTML classes.
- 2Match job IDs with /job/([A-Za-z0-9]+) — never assume a fixed length or lowercase-only pattern.
- 3Retry the 'Page Unavailable' placeholder up to 3 times with a ~500ms delay before giving up.
- 4Deduplicate listings by job ID, since featured jobs are repeated at the top of the board.
- 5Space requests ~500ms apart and cap concurrent detail fetches at ~3 to stay within the board's tolerance.
- 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 accesscURL
curl "https://connect.jobo.world/api/jobs?sources=jobvite" \
-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 Jobvite
Access Jobvite
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