- criticalWhy does a company search return other businesses' jobs?
- The companyName parameter is a fuzzy search, not an exact filter. Normalise and compare the companyName on every returned row against the employer you asked for, keep only exact matches, and count the rest as deliberate exclusions rather than silently importing them.
- highWhy does the URL slug not match the real company name?
- Job URLs are tenantless — the slug is generated from the title and location. In one 771-row audit a normalised slug matched the API's company name for only 639 rows; the rest were numbered accounts, renamed businesses, and punctuation differences. Take the name from the API.
- mediumIs the pagination total the employer's job count?
- No. The total describes the complete fuzzy traversal, not the exact employer's board. Preserve it as the authoritative total for the traversal so you know when paging finished, but publish the count of exactly-matched rows as the employer's job count.
- mediumCan I build the after cursor myself?
- No. Pagination is Relay-style with an opaque endCursor, so any value you construct will be rejected or silently reset the walk. Carry the endCursor forward exactly as received, and stop as soon as hasNextPage is false or the cursor is absent.
Wizehire Jobs API.
Pull small-business openings from Wizehire through its public jobseeker API, which returns full descriptions and compensation but filters by company name fuzzily — so every row needs an exact re-check.
What's in every response.
Data fields, real-world applications, and the companies already running on Wizehire.
Data fields
- Full Job Descriptions
- Structured Compensation
- Responsibilities & Qualifications
- Company Profile Fields
- Relay Cursor Pagination
- Remote & Location Flags
Use cases
- 01Small Business Job Boards
- 02Local Hiring Feeds
- 03Franchise & Trades Recruitment
- 04Compensation Benchmarking
Trusted by
- ARK Hospitality
- C&L Autobody
- CAMCO Property Management
How to scrape Wizehire.
Step-by-step guide to extracting jobs from Wizehire-powered career pages—endpoints, authentication, and working code.
import re
from urllib.parse import urlparse, parse_qs
BOARD_HOST = "jobs.wizehire.com"
JOB_ID = re.compile(r"-(?P<job_id>[0-9a-f]{16})$")
def parse_wizehire(url: str) -> dict:
parsed = urlparse(url)
host = parsed.netloc.lower()
# The legacy jobseeker host normalises to the canonical board host.
if host not in (BOARD_HOST, "jobseeker.wizehire.com"):
raise ValueError("not a Wizehire URL")
parts = [p for p in parsed.path.strip("/").split("/") if p]
if len(parts) == 2 and parts[0] == "job":
match = JOB_ID.search(parts[1])
if not match:
raise ValueError("job slug did not end in a 16-hex job id")
return {"kind": "job", "job_id": match.group("job_id")}
company = (parse_qs(parsed.query).get("companyName") or [None])[0]
if company:
return {"kind": "board", "company_name": company}
raise ValueError("a bare Wizehire search has no employer scope")
print(parse_wizehire("https://jobs.wizehire.com/job/director-of-sales-in-wichita-ks-us-e6e2576e1496fdfa"))
# {'kind': 'job', 'job_id': 'e6e2576e1496fdfa'}import requests
API = "https://api.jobseeker.wizehire.com/api/v1/jobs"
session = requests.Session()
session.headers["Accept"] = "application/json"
def prove_employer(job_id: str) -> dict | None:
resp = session.get(f"{API}/{job_id}", timeout=30)
if resp.status_code in (404, 410):
return None
resp.raise_for_status()
job = (resp.json() or {}).get("job") or {}
if job.get("id") != job_id or not job.get("companyName"):
return None
return {
"job_id": job_id,
"company_name": job["companyName"].strip(),
"canonical_job_url": job.get("canonicalJobUrl") or job.get("url"),
}
proof = prove_employer("e6e2576e1496fdfa")
print(proof)from urllib.parse import quote
PAGE_SIZE = 100
def fetch_pages(company_name: str):
after = None
while True:
url = f"{API}?first={PAGE_SIZE}&companyName={quote(company_name)}"
if after:
url += f"&after={quote(after)}"
resp = session.get(url, timeout=30)
resp.raise_for_status()
payload = resp.json()
pagination = payload.get("pagination") or {}
yield payload.get("jobs") or [], pagination
if not pagination.get("hasNextPage") or not pagination.get("endCursor"):
return
after = pagination["endCursor"]
for batch, pagination in fetch_pages("ARK Hospitality"):
print(len(batch), "rows of", pagination.get("total"))import unicodedata
def normalise(name: str) -> str:
folded = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
return " ".join(folded.lower().replace("&", "and").split())
def collect(company_name: str) -> dict:
target = normalise(company_name)
kept, excluded, total = [], 0, None
for batch, pagination in fetch_pages(company_name):
total = pagination.get("total", total)
for job in batch:
if normalise(job.get("companyName") or "") != target:
excluded += 1 # a fuzzy-search neighbour, not this employer
continue
kept.append(job)
# The API total describes the fuzzy traversal, not this employer's board.
return {"jobs": kept, "excluded": excluded, "fuzzy_total": total}
result = collect("ARK Hospitality")
print(f"{len(result['jobs'])} exact rows, {result['excluded']} excluded")def to_job(job: dict) -> dict | None:
if job.get("isClosed"):
return None
compensation = job.get("compensation") or {}
return {
"id": job.get("id"),
"title": job.get("title"),
"company": job.get("companyName"),
"description_html": job.get("description"),
"responsibilities": job.get("responsibilities"),
"qualifications": job.get("qualifications"),
"salary_min": compensation.get("min"),
"salary_max": compensation.get("max"),
"salary_interval": compensation.get("interval"),
"salary_raw": compensation.get("raw"),
"city": job.get("city"),
"state": job.get("state"),
"country": job.get("country"),
"fully_remote": job.get("fullyRemote", False),
"posted_at": job.get("publishedAt"),
"listing_url": job.get("canonicalJobUrl") or job.get("url"),
}
open_jobs = [mapped for job in result["jobs"] if (mapped := to_job(job))]
for job in open_jobs[:3]:
print(job["title"], "|", job["salary_min"], "-", job["salary_max"], job["salary_interval"])- 1Prove the employer from the API's companyName, never from the URL slug
- 2Re-check every returned row for an exact company match before emitting it
- 3Carry the opaque endCursor forward rather than constructing your own cursor
- 4Publish the exact-match count as the employer total, not the fuzzy traversal total
- 5Normalise jobseeker.wizehire.com URLs to the canonical jobs.wizehire.com host
- 6Skip per-job detail requests — listing rows already carry the full description
One endpoint. All Wizehire jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=wizehire" \
-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 Wizehire
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.