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.
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.
- Full Job Descriptions
- Structured Locations
- Requisition Numbers
- Department & Category
- Salary & Employment Type
- Posted & Expiry Dates
- 01Enterprise Job Monitoring
- 02Competitive Talent Intelligence
- 03Career Page Aggregation
- 04Salary Benchmarking
How to scrape Jobvite.
Step-by-step guide to extracting jobs from Jobvite-powered career pages—endpoints, authentication, and working code.
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")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")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_jobsimport 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"])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", [])))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.
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.
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.
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.
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.
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.
- 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.
One endpoint. All Jobvite jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=jobvite" \
-H "X-Api-Key: YOUR_KEY" 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.