OCCY Jobs API.
Pull structured UK job listings — salary bands, locations and full multi-section descriptions — from any OCCY-hosted career page through one public REST API, no login 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 OCCY.
- Structured Salary Bands
- Location & Postcode Data
- Multi-Section Descriptions
- Employment & Remote Status
- Job Category & Type
- Post & Close Dates
- 01UK Job Market Aggregation
- 02Salary Benchmarking
- 03Careers Page Monitoring
- 04Recruitment Data Pipelines
How to scrape OCCY.
Step-by-step guide to extracting jobs from OCCY-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse
def resolve_board(url: str):
"""Return (board_kind, company_key) for an OCCY board URL."""
parsed = urlparse(url)
host = (parsed.hostname or "").lower()
if host.endswith(".cp.occy.com"):
# Legacy company host: the subdomain is the tenant key (lowercase).
return "legacy", host[: -len(".cp.occy.com")]
if host == "app.occy.com":
# Shared host: /p/jobs/{ULID}; the 26-char key is case-sensitive.
segments = [s for s in parsed.path.split("/") if s]
if len(segments) == 3 and segments[:2] == ["p", "jobs"]:
return "shared", segments[2].upper()
raise ValueError(f"Unrecognised OCCY board URL: {url}")import requests
BASE = "https://api.occy.com/api/public"
def list_jobs(kind: str, company_key: str):
# OCCY enforces perPage <= 50 on both listing endpoints.
if kind == "legacy":
path, key_param = "careersite/company/jobs", "domain"
else:
path, key_param = "jobs", "companyId"
page, rows = 1, []
while True:
resp = requests.get(
f"{BASE}/{path}",
params={
key_param: company_key,
"perPage": 50,
"page": page,
"sort": "createdAt",
"sortDir": "DESC",
},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
batch = data.get("jobs", [])
rows.extend(batch)
total = data.get("total", 0)
per_page = data.get("perPage") or len(batch)
consumed = (page - 1) * per_page + len(batch)
if not batch or consumed >= total:
break
page += 1
return rowsdef posting_ids(kind: str, rows: list[dict]) -> list[str]:
ids: list[str] = []
if kind == "legacy":
# A legacy job can carry several postings.
for job in rows:
for posting in job.get("postingDetails", []):
pid = (posting.get("id") or "").strip()
if pid:
ids.append(pid)
else:
# Shared-host rows carry the posting key directly.
for job in rows:
pid = (job.get("posting_id") or "").strip()
if pid:
ids.append(pid)
return idsdef get_details(posting_id: str, kind: str, company_key: str):
# Legacy detail calls include ?domain=; shared-host calls omit it.
params = {"domain": company_key} if kind == "legacy" else {}
resp = requests.get(
f"{BASE}/jobs/posting/{posting_id}/details",
params=params,
timeout=30,
)
if resp.status_code in (404, 410):
return None # posting was removed
resp.raise_for_status()
return resp.json()
# End-to-end
kind, key = resolve_board("https://wearewithyou.cp.occy.com/p/jobs")
rows = list_jobs(kind, key)
for pid in posting_ids(kind, rows):
details = get_details(pid, kind, key)
if details:
print(details.get("title"))Cap perPage at 50 and page with the page parameter, using the echoed total and perPage to decide when to stop.
Preserve the exact upstream case of the ULID (keep it uppercase) and only lowercase legacy *.cp.occy.com subdomain tenants.
Detect the board type from the URL host and branch: *.cp.occy.com uses the domain query, app.occy.com/p/jobs/{ULID} uses the companyId query.
Always use posting_id / jobPosting.id as the stable public posting key and keep the other identifiers as metadata only.
Treat 404/410 from jobs/posting/{id}/details as a removed posting and skip it rather than failing the whole run.
- 1Cap perPage at 50 and keep paging while (page - 1) * perPage + returned < total.
- 2Route by URL host: subdomain tenants use the domain query, app.occy.com ULIDs use companyId.
- 3Preserve shared-host ULID case exactly; lowercasing it triggers HTTP 400.
- 4Key every job on posting_id (jobPosting.id); treat id, job_id and jobId as metadata only.
- 5Send sort=createdAt&sortDir=DESC so new postings surface first and can be detected without a full re-crawl.
- 6Space out detail requests and back off on HTTP 429; no auth or cookies are required.
One endpoint. All OCCY jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=occy" \
-H "X-Api-Key: YOUR_KEY" Access OCCY
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.