# Oracle Recruiting Jobs API: Get Oracle Cloud HCM Jobs as JSON

> Oracle Recruiting Cloud career sites run on a keyless REST API. Find the host and site number, page the finder, and fetch full descriptions per job.

Source: https://cleanjobdata.com/articles/how-to-scrape-oracle-recruiting-job-listings
Published: 2026-11-01

TL;DR: Oracle Recruiting Cloud (part of Oracle Fusion Cloud HCM) career sites load jobs from a keyless REST resource, recruitingCEJobRequisitions, with paging set inside the finder parameter. You need the tenant host and the career site number, and a wrong site number returns a different job set instead of an error. The list has titles, dates and locations but no description, so each job needs a recruitingCEJobRequisitionDetails call. There's no salary field, and the remote flag is only filled on some postings.

To get a company's jobs from Oracle Recruiting Cloud, call `https://{host}/hcmRestApi/resources/latest/recruitingCEJobRequisitions?onlyData=true&finder=findReqs;siteNumber={site},limit=100,offset=0` with no API key. The list returns titles, dates and locations, and each job's full description comes from one `recruitingCEJobRequisitionDetails` request.

That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain an Oracle Recruiting scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the [Jobs API docs](https://cleanjobdata.com/docs/api/jobs).

## At a Glance

| | |
|---|---|
| Endpoint | `GET https://{host}/hcmRestApi/resources/latest/recruitingCEJobRequisitions?onlyData=true&finder=findReqs;siteNumber={site},limit=100,offset=0` |
| Detail endpoint | `GET https://{host}/hcmRestApi/resources/latest/recruitingCEJobRequisitionDetails?expand=all&onlyData=true&finder=ById;Id={id},siteNumber={site}` |
| API key | None |
| Pagination | `limit` and `offset` inside `finder`, 100 per page |
| Full description | Detail call, split across three HTML fields |
| Salary field | No |
| Remote | `WorkplaceType`, filled on some postings only |

## Finding the Host and Site Number

Oracle's candidate-facing career sites have URLs like:

```
https://acme.fa.us2.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1/
```

You need two values from that URL:

- **Host.** `acme.fa.us2.oraclecloud.com`. The `fa.us2` part is a shared Oracle Cloud pod, and other regions such as `fa.us6` and `fa.ca2` exist too.
- **Site number.** The segment after `/sites/`, here `CX_1`. Sites are named `CX`, `CX_1`, `CX_2`, `CX_1001` and so on, and some have names like `jobsearch`.

Copy the site number exactly. A wrong one doesn't fail: on one tenant, the real site listed 86 jobs while a made-up site number returned 200 with 150 jobs drawn from the tenant's other career sites. If a company embeds the career site on its own domain, open the Network tab and look for requests to `hcmRestApi`.

## The List Request

```bash
curl "https://acme.fa.us2.oraclecloud.com/hcmRestApi/resources/latest/recruitingCEJobRequisitions?onlyData=true&expand=requisitionList.secondaryLocations&finder=findReqs;siteNumber=CX_1,limit=100,offset=0,sortBy=POSTING_DATES_DESC"
```

The paging parameters go inside `finder`, separated by commas. `expand=requisitionList.secondaryLocations` adds a job's extra locations, and `sortBy=POSTING_DATES_DESC` puts the newest jobs first.

The response is a search envelope: `items[0]` holds the search settings and the total, and the jobs sit in `items[0].requisitionList`.

```json
{
  "items": [
    {
      "SiteNumber": "CX_1",
      "TotalJobsCount": 842,
      "requisitionList": [
        {
          "Id": "753717",
          "Title": "Store Associate",
          "PostedDate": "2026-11-01",
          "PrimaryLocation": "Bristol, VT, United States",
          "PrimaryLocationCountry": "US",
          "ShortDescriptionStr": "7 MAIN ST 05443 BRISTOL,VT UNITED STATES",
          "WorkplaceType": "",
          "JobSchedule": null,
          "secondaryLocations": []
        }
      ]
    }
  ],
  "count": 1,
  "hasMore": false
}
```

| Field | What it is |
|---|---|
| `Id` | Requisition ID, the same value in the list and the detail call. Not always numeric |
| `Title` | Job title |
| `PostedDate` | Posting date, date only |
| `PrimaryLocation` | Comma-separated location text |
| `PrimaryLocationCountry` | ISO alpha-2 country code |
| `secondaryLocations` | Extra locations with `Name`, `CountryCode`, `Latitude`, `Longitude` (needs `expand`) |
| `ShortDescriptionStr` | Short text set by the tenant, not the description. On one tenant it was the street address |
| `WorkplaceType` | For example `"Hybrid"`, often an empty string |
| `JobSchedule` | For example `"Full time"` or `"Part time"`, when set |
| `TotalJobsCount` | On `items[0]`, the total for this site |

The outer `count` and `hasMore` describe the envelope, not the jobs: a site with thousands of jobs still reports `"count": 1` and `"hasMore": false`. Use `TotalJobsCount` instead.

## Pagination

Increase `offset` inside the finder by the number of rows you actually received, and stop on an empty `requisitionList` or when `offset` reaches `TotalJobsCount`. A page size of 100 works well.

Dedupe by `Id`. Offset paging over a date sort isn't stable: a job posted while you're paging shifts the window, and the same `Id` can show up on two pages.

## The Detail Request

```bash
curl "https://acme.fa.us2.oraclecloud.com/hcmRestApi/resources/latest/recruitingCEJobRequisitionDetails?expand=all&onlyData=true&finder=ById;Id=753717,siteNumber=CX_1"
```

The job is `items[0]`. Fields worth knowing:

- **`ExternalDescriptionStr`, `ExternalResponsibilitiesStr`, `ExternalQualificationsStr`.** The posting body, as HTML. Join whichever are non-empty, in that order. The last two are often empty strings.
- **`CorporateDescriptionStr`, `OrganizationDescriptionStr`.** Company boilerplate that repeats on every job for the tenant. Leave them out unless you want it.
- **`ExternalPostedStartDate`.** A full ISO timestamp such as `2026-11-01T11:31:07+00:00`. Prefer it over the list's date-only `PostedDate`.
- **`ExternalPostedEndDate`.** The scheduled end of the posting. Treat it as a hint and detect closed jobs by comparing lists.
- **`JobSchedule`, `JobLevel`, `Category`, `JobFunction`.** Classification fields, filled per tenant.
- **`primaryLocationCoordinates`.** Latitude and longitude for `PrimaryLocation`, as strings.
- **`workLocation`.** A facility record. It can name a different place from the advertised location, so don't use it as the job's location.
- **`requisitionFlexFields`.** Tenant-specific `Prompt` / `Value` pairs.

The API has no apply link. The candidate-facing job page is:

```
https://{host}/hcmUI/CandidateExperience/en/sites/{site}/job/{Id}
```

## Fetching Jobs (Python)

```python
import requests

def list_oracle_jobs(host: str, site: str) -> list[dict]:
    base = f"https://{host}/hcmRestApi/resources/latest"
    seen, jobs, offset = set(), [], 0
    while True:
        finder = f"findReqs;siteNumber={site},limit=100,offset={offset},sortBy=POSTING_DATES_DESC"
        resp = requests.get(
            f"{base}/recruitingCEJobRequisitions",
            params={"onlyData": "true", "expand": "requisitionList.secondaryLocations", "finder": finder},
            timeout=25,
        )
        resp.raise_for_status()
        envelope = (resp.json().get("items") or [{}])[0]
        rows = envelope.get("requisitionList") or []
        total = envelope.get("TotalJobsCount") or 0
        if not rows:
            break
        for row in rows:
            job_id = str(row["Id"])
            if job_id not in seen:
                seen.add(job_id)
                jobs.append(row)
        offset += len(rows)
        if total and offset >= total:
            break
    return jobs

def get_oracle_detail(host: str, site: str, job_id: str) -> dict | None:
    resp = requests.get(
        f"https://{host}/hcmRestApi/resources/latest/recruitingCEJobRequisitionDetails",
        params={"expand": "all", "onlyData": "true", "finder": f"ById;Id={job_id},siteNumber={site}"},
        timeout=20,
    )
    resp.raise_for_status()
    items = resp.json().get("items") or []
    if not items:
        return None  # the requisition is gone
    d = items[0]
    parts = [d.get(k) for k in ("ExternalDescriptionStr", "ExternalResponsibilitiesStr", "ExternalQualificationsStr")]
    return {
        "id": job_id,
        "title": d.get("Title"),
        "description_html": "\n".join(p for p in parts if p and p.strip()),
        "published": d.get("ExternalPostedStartDate"),
        "location": d.get("PrimaryLocation"),
        "country": d.get("PrimaryLocationCountry"),
        "workplace_type": d.get("WorkplaceType") or None,
        "url": f"https://{host}/hcmUI/CandidateExperience/en/sites/{site}/job/{job_id}",
    }
```

`requests` URL-encodes the `finder` value, and Oracle accepts it either way.

## Gotchas

**`PrimaryLocation` comes in three shapes.** `"Abilene, TX, United States"` is city, region and country. `"United States"` is a country alone. Two parts are ambiguous: `"TX, United States"` is a region, but `"Suva, Fiji"` is a city. Take the country from `PrimaryLocationCountry` and check the first segment against a gazetteer before you call it a city. See the [location normalization guide](https://cleanjobdata.com/guides/job-location-normalization).

**Coordinates change type.** `Latitude` and `Longitude` are numbers in the list and strings in the detail response, so convert them before comparing.

**IDs aren't always numbers.** Some tenants use IDs like `W734354`. Store `Id` as a string.

**Gone jobs return 200.** A closed requisition's detail call answers 200 with an empty `items` array, not a 404.

**One tenant can expose the same jobs on several sites.** Some hosts publish the same requisitions under more than one site number. Opening a job page under a duplicate site answers with a 302 redirect to the main site, but the listing page doesn't redirect, so check a few job pages. Other sites mix shared jobs with jobs of their own. Dedupe by host plus `Id`.

**`WorkplaceType` is patchy.** It's set to values like `"Hybrid"` on some postings and empty on others from the same tenant, so also check the title and location for "remote" or "hybrid".

**No employer name or salary.** The payloads don't include the company name or any pay field, so you supply the name yourself and look for pay in the description text.

## Candidate Experience API vs Oracle's Authenticated REST API

These resources sit inside Oracle's HCM REST API. Most of that API needs an authenticated user from the employer's Oracle Cloud instance and covers internal HR and recruiting data. The candidate-experience resources used here answer anonymous requests because the public career site depends on them, and they only return published jobs. If you want postings from companies you don't work with, these are the ones you can use.

## When This Is Enough

For one employer whose host and site number you know, the list call plus one detail call per new job is straightforward, and large tenants page cleanly at 100 rows.

## When It Isn't

Across many Oracle tenants you'll deal with duplicate sites, ambiguous locations and per-tenant field use, alongside other platforms like [Workday](https://cleanjobdata.com/articles/how-to-scrape-workday-job-listings) and [SmartRecruiters](https://cleanjobdata.com/articles/how-to-scrape-smartrecruiters-job-listings). See [which ATS have a public job postings API](https://cleanjobdata.com/articles/ats-public-job-postings-api-list) for how they compare.

CleanJobData reads Oracle Recruiting sites alongside other platforms and returns one schema, with locations resolved to cities, states and countries and a remote type. There's no salary in Oracle's data, so that field can be empty. Try it in the [Playground](https://cleanjobdata.com/playground) or see the [API Documentation](https://cleanjobdata.com/docs).

## Skip the scraper: get this data from the CleanJobData API

If you'd rather not maintain an Oracle Recruiting collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in `pagination.next_page`.

```bash
curl "https://api.cleanjobdata.com/jobs?title=engineer&domain=acme.fa.us2.oraclecloud.com&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"
```

The `domain` filter is an exact match on the host of each job's `application_url`. Oracle hosts belong to one tenant, so this scopes the query to that company; `company_website_url` and `employer_id` work too. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

- [Jobs API reference](https://cleanjobdata.com/docs/api/jobs) for every filter and response field
- [Playground](https://cleanjobdata.com/playground) to run queries without writing code
- [Pricing](https://cleanjobdata.com/pricing) — the Trial is free with no card required

## FAQ

### Does Oracle Recruiting Cloud have a public jobs API?

Yes. Career sites built on Oracle Recruiting load jobs from `https://{host}/hcmRestApi/resources/latest/recruitingCEJobRequisitions`, which needs no API key. You pass the career site number and paging values inside the `finder` parameter.

### How do I find the Oracle site number?

It's the segment after `/sites/` in the career site URL, for example `CX_1` in `/hcmUI/CandidateExperience/en/sites/CX_1/`. Copy it exactly, because a wrong site number returns a different set of jobs instead of an error.

### How do I get the full job description from Oracle?

Call `recruitingCEJobRequisitionDetails` with `finder=ById;Id={id},siteNumber={site}`. Join `ExternalDescriptionStr`, `ExternalResponsibilitiesStr` and `ExternalQualificationsStr`, skipping the empty ones.

### Does the Oracle Recruiting API include salary?

No field we read carries pay. If an employer publishes a range, it's in the description HTML.
