- mediumWhy does an AvaHR URL return HTTP 410 rather than 404?
- AvaHR retires both boards and individual jobs with an explicit 410 Gone. That is canonical removal evidence for the vacancy, but the numeric company ID remains a valid historical identity — keep the employer resolvable and mark the posting stale.
- highShould I key the employer on the company slug?
- No. The slug is display text and can be rewritten. The numeric company ID that prefixes it is the stable tenant key, and it appears in every first-party URL, so parse both and store the number.
- mediumHow do I confirm a page is really an AvaHR board?
- Require first-party ownership proof in the markup — the page references the vendor's own site — and require every parsed row to resolve back to the same numeric company ID. A page that matches the URL shape but carries neither is not a board.
- lowHow does pagination work on a large AvaHR board?
- It does not. The board returns the employer's complete vacancy collection in a single server-rendered page, so there is no cursor to follow. If you find yourself building a page loop, you are on the wrong page — check that the URL ends in /jobs/.
VIVAHR / AvaHR Jobs API.
Extract an employer's complete vacancy list from a VIVAHR (AvaHR) board in one request, keyed on the numeric company ID that sits in every first-party URL, with JobPosting JSON-LD per job.
What's in every response.
Data fields, real-world applications, and the companies already running on VIVAHR / AvaHR.
Data fields
- Complete Board In One Page
- JobPosting JSON-LD
- Numeric Company IDs
- Direct Apply URLs
- Employer Branding Data
- Explicit HTTP 410 Removals
Use cases
- 01SMB Job Aggregation
- 02Healthcare & Trades Hiring Feeds
- 03Careers Page Monitoring
- 04ATS Data Pipelines
Trusted by
- Adelante Healthcare
- American Roofing & Waterproofing
- Arizona Biltmore Dentistry
- Zzeeks Pizza
How to scrape VIVAHR / AvaHR.
Step-by-step guide to extracting jobs from VIVAHR / AvaHR-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse
PUBLIC_HOST = "jobs.avahr.com"
def split_id_slug(segment: str) -> tuple[str, str]:
head, _, tail = segment.partition("-")
if not head.isdigit() or not tail:
raise ValueError(f"expected {{id}}-{{slug}}, got {segment!r}")
return head, tail
def parse_avahr(url: str) -> dict:
parsed = urlparse(url)
if parsed.netloc.lower() != PUBLIC_HOST:
raise ValueError("not an AvaHR host")
parts = [p for p in parsed.path.strip("/").split("/") if p]
if not parts:
raise ValueError("no company segment")
company_id, company_slug = split_id_slug(parts[0])
job_id = None
if len(parts) == 2 and parts[1] != "jobs":
job_id, _ = split_id_slug(parts[1])
elif len(parts) > 2:
# /apply/ and other subroutes are not identities.
raise ValueError("unsupported AvaHR subroute")
return {
"company_id": company_id,
"company_slug": company_slug,
"job_id": job_id,
"board_url": f"https://{PUBLIC_HOST}/{company_id}-{company_slug}/jobs/",
}
print(parse_avahr("https://jobs.avahr.com/8147-zzeeks-pizza/78012-delivery-driver"))import requests
from bs4 import BeautifulSoup
session = requests.Session()
session.headers["Accept"] = "text/html,application/xhtml+xml"
def fetch_board(board_url: str) -> BeautifulSoup:
resp = session.get(board_url, timeout=30)
if resp.status_code in (404, 410):
raise LookupError("this AvaHR board no longer exists")
resp.raise_for_status()
# First-party ownership proof: the page references the vendor's own site.
if "https://avahr.com" not in resp.text.lower():
raise RuntimeError("page carried no AvaHR ownership proof")
return BeautifulSoup(resp.text, "html.parser")
board = parse_avahr("https://jobs.avahr.com/8677-adelante-healthcare/jobs/")
soup = fetch_board(board["board_url"])
print(soup.select_one("div.company-logo h3").get_text(strip=True))from urllib.parse import urljoin
def parse_rows(board: dict, soup: BeautifulSoup) -> list[dict]:
rows = []
for listing in soup.select("div.listing"):
anchor = next((a for a in listing.select("div.text a[href]") if a.select_one("h2")), None)
if anchor is None:
continue
url = urljoin(f"https://{PUBLIC_HOST}/", anchor["href"])
identity = parse_avahr(url)
if identity["company_id"] != board["company_id"] or not identity["job_id"]:
continue # a row that belongs to another company is a rejection
meta = listing.select_one("div.text h3")
apply_anchor = listing.select_one("div.apply a[href]")
rows.append({
"job_id": identity["job_id"],
"title": anchor.select_one("h2").get_text(strip=True),
"meta": meta.get_text(" ", strip=True) if meta else None,
"listing_url": url,
"apply_url": urljoin(f"https://{PUBLIC_HOST}/", apply_anchor["href"]) if apply_anchor else url,
})
return rows
listings = parse_rows(board, soup)
print(f"{len(listings)} open jobs")import json
import time
def find_job_posting(soup: BeautifulSoup) -> dict | 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":
return node
return None
def hydrate(board: dict, row: dict) -> dict | None:
resp = session.get(row["listing_url"], timeout=30)
if resp.status_code in (404, 410):
return None # AvaHR uses 410 heavily for retired jobs
resp.raise_for_status()
page = BeautifulSoup(resp.text, "html.parser")
if "https://avahr.com" not in resp.text.lower():
raise RuntimeError("detail page contradicted its first-party identity")
posting = find_job_posting(page)
if posting is None:
raise RuntimeError("detail page carried no JobPosting JSON-LD")
salary = (posting.get("baseSalary") or {}).get("value") or {}
apply_anchor = page.select_one("div.apply-btn a[href]")
return {
"job_id": row["job_id"],
"company_id": board["company_id"],
"title": posting.get("title"),
"description_html": posting.get("description"),
"posted_at": posting.get("datePosted"),
"closes_at": posting.get("validThrough"),
"employment_type": posting.get("employmentType"),
"salary_min": salary.get("minValue"),
"salary_max": salary.get("maxValue"),
"listing_url": row["listing_url"],
"apply_url": apply_anchor["href"] if apply_anchor else row["apply_url"],
}
for row in listings[:3]:
print(hydrate(board, row)["title"])
time.sleep(0.1)def classify(url: str) -> str:
try:
resp = session.head(url, timeout=15, allow_redirects=True)
except requests.Timeout:
return "unreachable" # retry later; not evidence of anything
if resp.status_code == 410:
return "retired" # identity stays valid, vacancy is gone
if resp.status_code == 404:
return "not_found"
if resp.status_code < 400:
return "live"
return "error"
for url in (board["board_url"],
"https://jobs.avahr.com/8147-zzeeks-pizza/78012-delivery-driver"):
print(classify(url), url)- 1Key the employer on the numeric company ID, never on the mutable slug
- 2Fetch the board once — the collection is complete and unpaginated
- 3Require first-party ownership proof before parsing any page as a board
- 4Reject listing rows whose URL resolves to a different company ID
- 5Read descriptions and salary from JobPosting JSON-LD rather than the rendered markup
- 6Treat HTTP 410 as a retired vacancy while keeping the employer identity resolvable
One endpoint. All VIVAHR / AvaHR jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=vivahr / avahr" \
-H "X-Api-Key: YOUR_KEY"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.
Access VIVAHR / AvaHR
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.