- highRequesting more than 50 results per page returns HTTP 400. OCCY's public API enforces a perPage maximum of 50 on both listing endpoints.
- Cap perPage at 50 and page with the page parameter, using the echoed total and perPage to decide when to stop.
- highShared-host boards (app.occy.com/p/jobs/{ULID}) use case-sensitive 26-character company keys; the lowercased form of a key returns HTTP 400.
- Preserve the exact upstream case of the ULID (keep it uppercase) and only lowercase legacy *.cp.occy.com subdomain tenants.
- mediumOCCY runs two board generations with different endpoints and JSON shapes — legacy careersite/company/jobs (camelCase, postings nested under postingDetails[]) and shared-host jobs (snake_case, posting_id inline).
- 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.
- mediumEach job exposes several identifiers (id, job_id, jobPosting.jobId) alongside the posting key; keying on the wrong one breaks detail lookups and deduplication.
- Always use posting_id / jobPosting.id as the stable public posting key and keep the other identifiers as metadata only.
- lowA posting can be pulled after it appears in a listing, returning HTTP 404 or 410 from the detail endpoint.
- Treat 404/410 from jobs/posting/{id}/details as a removed posting and skip it rather than failing the whole run.
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.
What's in every response.
Data fields, real-world applications, and the companies already running on OCCY.
Data fields
- Structured Salary Bands
- Location & Postcode Data
- Multi-Section Descriptions
- Employment & Remote Status
- Job Category & Type
- Post & Close Dates
Use cases
- 01UK Job Market Aggregation
- 02Salary Benchmarking
- 03Careers Page Monitoring
- 04Recruitment Data Pipelines
DIY GUIDE
How to scrape OCCY.
Step-by-step guide to extracting jobs from OCCY-powered career pages—endpoints, authentication, and working code.
Step 1: Identify the board type and company key
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}")Step 2: Page through the public listing endpoint
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 rowsStep 3: Extract the immutable posting IDs
def 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 idsStep 4: Fetch full details for each posting
def 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")) Common issues
Best practices
- 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.
Or skip the complexity
One endpoint. All OCCY jobs. No scraping, no sessions, no maintenance.
Get API accesscURL
curl "https://connect.jobo.world/api/jobs?sources=occy" \
-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 OCCY
Access OCCY
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