All platforms

Oracle Cloud Jobs API.

Pull enterprise and Fortune 500 job requisitions straight from Oracle's HCM Recruiting Cloud REST API — paginated JSON listings plus a details endpoint for full descriptions, skills, and workplace metadata.

Get API access
Oracle Cloud
Live
250K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Oracle Cloud
OracleJPMorgan ChaseGoldman SachsMacy'sMarriott
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.

What's in every response.

Data fields, real-world applications, and the companies already running on Oracle Cloud.

Data fields
  • Full HTML Job Descriptions
  • Qualifications & Responsibilities
  • Structured Skills List
  • Department & Business Unit
  • Workplace Type & Schedule
  • Custom Flex Fields
Use cases
  1. 01Enterprise Job Aggregation
  2. 02Fortune 500 Hiring Tracking
  3. 03Global Talent Market Monitoring
  4. 04Large-Scale Requisition Extraction
Trusted by
OracleJPMorgan ChaseGoldman SachsMacy'sMarriottFordTexas InstrumentsSherwin-Williams
DIY GUIDE

How to scrape Oracle Cloud.

Step-by-step guide to extracting jobs from Oracle Cloud-powered career pages—endpoints, authentication, and working code.

RESTintermediateNo published limit; ~300ms between requests, max ~3 concurrent recommendedNo auth

Resolve the site number

Every Oracle Cloud API call is scoped to a siteNumber (e.g. CX_1). A /sites/<segment> path that already starts with 'CX' is the siteNumber itself; vanity slugs like /sites/jobsearch embed the real value in the page HTML, and most production tenants default to CX_1.

Step 1: Resolve the site number
import re
import requests
from urllib.parse import urlparse

SITE_NUMBER_PATTERNS = [
    re.compile(r"""siteNumber\s*[:=]\s*['"]([^'"]+)['"]""", re.IGNORECASE),
    re.compile(r'"siteNumber"\s*:\s*"([^"]+)"', re.IGNORECASE),
    re.compile(r"siteNumber=(CX_[^&\"'\s]+)", re.IGNORECASE),
]

def resolve_site_number(careers_url: str) -> str:
    """Resolve the Oracle Cloud siteNumber for a careers URL."""
    # A /sites/<segment> that starts with "CX" IS the siteNumber.
    path = urlparse(careers_url).path
    match = re.search(r"/sites/([^/?#]+)", path, re.IGNORECASE)
    if match and match.group(1).upper().startswith("CX"):
        return match.group(1)

    # Vanity slugs embed the real siteNumber in the careers-page HTML.
    html = requests.get(careers_url, timeout=30).text
    for pattern in SITE_NUMBER_PATTERNS:
        found = pattern.search(html)
        if found:
            return found.group(1)

    # Most production Oracle tenants default to CX_1.
    return "CX_1"

careers_url = "https://eeho.fa.us2.oraclecloud.com/hcmUI/CandidateExperience/en/sites/jobsearch/jobs"
site_number = resolve_site_number(careers_url)
print(f"Site number: {site_number}")

Fetch job listings from the API

Call recruitingCEJobRequisitions for a page of summaries. Critically, limit and offset must live INSIDE the finder param — passed as top-level query params Oracle silently ignores them and every page returns the same rows. Keep the finder's ';', '=' and ',' literal (unencoded), so build the query string by hand.

Step 2: Fetch job listings from the API
import uuid
import requests

DOMAIN = "eeho.fa.us2.oraclecloud.com"
SITE_NUMBER = "CX_1"
LISTINGS_PATH = "/hcmRestApi/resources/latest/recruitingCEJobRequisitions"
PAGE_SIZE = 200  # the CE API caps the finder-scoped list at 200 per request

HEADERS = {
    "ora-irc-cx-userid": str(uuid.uuid4()),  # any UUID works for anonymous access
    "ora-irc-language": "en",
    "content-type": "application/vnd.oracle.adf.resourceitem+json;charset=utf-8",
}

def build_listings_url(domain: str, site_number: str, offset: int, limit: int = PAGE_SIZE) -> str:
    # limit/offset MUST be inside the finder; the finder's ';' '=' ',' are literal.
    finder = f"findReqs;siteNumber={site_number},limit={limit},offset={offset}"
    expand = (
        "requisitionList.workLocation,requisitionList.otherWorkLocations,"
        "requisitionList.secondaryLocations,flexFieldsFacet.values,"
        "requisitionList.requisitionFlexFields"
    )
    query = f"onlyData=true&expand={expand}&finder={finder}"
    return f"https://{domain}{LISTINGS_PATH}?{query}"

url = build_listings_url(DOMAIN, SITE_NUMBER, offset=0)
data = requests.get(url, headers=HEADERS, timeout=30).json()

# The response holds a single "search" item; the jobs live in requisitionList.
search_item = data["items"][0]
jobs = search_item.get("requisitionList", [])
total = search_item.get("TotalJobsCount", 0)
print(f"Fetched {len(jobs)} of {total} jobs")

Parse listing data and build job URLs

Each requisition carries an Id, Title, PrimaryLocation, PostedDate and a short summary. Build the canonical job URL from the domain, site path and Id — the listings feed only carries ShortDescriptionStr, so full content comes from the details call.

Step 3: Parse listing data and build job URLs
def build_job_url(domain: str, site_path: str, job_id: str) -> str:
    return f"https://{domain}/hcmUI/CandidateExperience/en/sites/{site_path}/job/{job_id}"

site_path = "jobsearch"  # the /sites/<segment> from the careers URL

for job in jobs[:5]:  # first 5 jobs
    print({
        "id": job.get("Id"),
        "title": job.get("Title"),
        "location": job.get("PrimaryLocation"),
        "secondary_locations": [loc.get("Name") for loc in job.get("secondaryLocations", [])],
        "posted_date": job.get("PostedDate"),
        "short_description": (job.get("ShortDescriptionStr") or "")[:100],
        "is_hot_job": job.get("HotJobFlag", False),
        "is_trending": job.get("TrendingFlag", False),
        "url": build_job_url(DOMAIN, site_path, job.get("Id")),
    })

Fetch full job details

Call recruitingCEJobRequisitionDetails with the job Id to get the full description, qualifications, responsibilities, structured skills and custom flex fields. The finder uses the ById form and, again, must keep its quotes and separators literal.

Step 4: Fetch full job details
import uuid
import requests

DETAILS_PATH = "/hcmRestApi/resources/latest/recruitingCEJobRequisitionDetails"

def get_job_details(domain: str, site_number: str, job_id: str) -> dict | None:
    """Fetch full job details from Oracle Cloud."""
    # Keep the finder's '"', ';', '=' and ',' literal — do not percent-encode them.
    finder = f'ById;Id="{job_id}",siteNumber={site_number}'
    query = f"expand=all&onlyData=true&finder={finder}"
    url = f"https://{domain}{DETAILS_PATH}?{query}"

    headers = {
        "ora-irc-cx-userid": str(uuid.uuid4()),
        "ora-irc-language": "en",
        "content-type": "application/vnd.oracle.adf.resourceitem+json;charset=utf-8",
    }

    data = requests.get(url, headers=headers, timeout=30).json()
    items = data.get("items") or []
    return items[0] if items else None

details = get_job_details(DOMAIN, SITE_NUMBER, "307750")
if details:
    print({
        "id": details.get("Id"),
        "title": details.get("Title"),
        "category": details.get("Category"),
        "department": details.get("Department"),
        "workplace_type": details.get("WorkplaceType"),
        "description": (details.get("ExternalDescriptionStr") or "")[:200],
        "qualifications": (details.get("ExternalQualificationsStr") or "")[:200],
        "responsibilities": (details.get("ExternalResponsibilitiesStr") or "")[:200],
        "skills": [s.get("Skill") for s in details.get("skills", [])],
        "flex_fields": {f.get("Prompt"): f.get("Value") for f in details.get("requisitionFlexFields", [])},
    })

Paginate on TotalJobsCount

Advance offset by the number of jobs returned and stop when offset reaches TotalJobsCount. Do NOT trust the top-level hasMore flag — it describes the single-item outer collection and reads false even mid-board, cutting pagination short.

Step 5: Paginate on TotalJobsCount
import time
import uuid
import requests

def fetch_all_jobs(domain: str, site_number: str, page_size: int = PAGE_SIZE) -> list:
    """Fetch every job, paginating on TotalJobsCount."""
    all_jobs = []
    offset = 0
    total = None

    while True:
        url = build_listings_url(domain, site_number, offset, page_size)
        data = requests.get(url, headers=HEADERS, timeout=30).json()

        items = data.get("items") or []
        if not items:
            break

        search_item = items[0]
        jobs = search_item.get("requisitionList") or []
        if not jobs:
            break

        all_jobs.extend(jobs)
        if total is None:
            total = search_item.get("TotalJobsCount") or 0
        print(f"Fetched {len(all_jobs)} of {total} jobs...")

        # Page on TotalJobsCount; the top-level hasMore is unreliable.
        offset += len(jobs)
        if total and offset >= total:
            break
        if not total and len(jobs) < page_size:  # defensive stop when count is absent
            break

        time.sleep(0.3)  # matches the scraper's 300ms inter-request delay

    return all_jobs

all_jobs = fetch_all_jobs(DOMAIN, SITE_NUMBER)
print(f"Total jobs collected: {len(all_jobs)}")
Common issues
criticalEvery page returns the same rows

limit and offset are ignored as top-level query params. They must live inside the finder, e.g. finder=findReqs;siteNumber=CX_1,limit=200,offset=200. Build the finder string by hand so its ';', '=' and ',' stay unencoded.

criticalWrong or missing site number returns zero rows

Resolve the siteNumber first: a /sites/<CX_*> path segment is the siteNumber itself, otherwise regex the careers-page HTML for 'siteNumber', and fall back to CX_1, which most production tenants use.

highPagination stops after the first page

The top-level hasMore flag describes the single-item outer collection and reads false even mid-board. Ignore it and page on TotalJobsCount, stopping only when offset reaches the total.

highOnly short descriptions in the listings feed

The listings API returns ShortDescriptionStr. Call recruitingCEJobRequisitionDetails with the job Id (finder=ById;Id="...",siteNumber=...) for ExternalDescriptionStr, ExternalQualificationsStr, ExternalResponsibilitiesStr, skills and flex fields.

mediumPer-company domains vary

Each tenant lives at {code}.fa.{datacenter}.oraclecloud.com, sometimes with an extra realm label (e.g. code.fa.em1.ukg.oraclecloud.com). Take the domain from the company's real careers URL rather than assuming a fixed host.

medium401 / 403 / 429 responses

401 means the board requires authentication (likely SSO-gated with no public listings), 403 means the request was blocked, and 429 is rate limiting. Throttle to ~300ms between requests, cap concurrency, and back off on 403/429.

Best practices
  1. 1Embed limit and offset inside the finder param — Oracle ignores them as top-level query params
  2. 2Resolve the siteNumber from the /sites/<CX_*> path or the careers-page HTML before any API call; fall back to CX_1
  3. 3Page on TotalJobsCount, never on the unreliable top-level hasMore flag
  4. 4Build finder query strings by hand so ';', '=', ',' and '"' stay literal (requests' params dict would encode them)
  5. 5Fetch full content from recruitingCEJobRequisitionDetails; listings only carry ShortDescriptionStr
  6. 6Throttle to ~300ms between requests with no more than ~3 concurrent detail calls to avoid 403/429
Or skip the complexity

One endpoint. All Oracle Cloud jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=oracle cloud" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access Oracle Cloud
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.

99.9%API uptime
<200msAvg response
50M+Jobs processed