SuccessFactors Jobs API: Get SAP Career Site Listings as JSON
TL;DR
SAP SuccessFactors Career Site Builder sites on a company subdomain of jobs.hr.cloud.sap often answer a keyless POST to /services/recruiting/v1/jobs with paged JSON, 10 jobs per page. Results are split by locale, so you query every locale the site offers and merge by job id. The list has no description or salary, so the full text comes from each job page's HTML, and dates arrive formatted for the locale you asked for; sites without the endpoint can be read from sitemap.xml or the /search?startrow=N pages.
On this page
To get a company's jobs from SAP SuccessFactors, send a POST to https://{company}.jobs.hr.cloud.sap/services/recruiting/v1/jobs with a JSON body containing locale and pageNumber. No API key is needed, and the response lists job IDs, titles, locations and start dates 10 at a time; the full description comes from each job's HTML page.
That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a SuccessFactors scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the Jobs API docs.
At a Glance
| Endpoint | POST https://{company}.jobs.hr.cloud.sap/services/recruiting/v1/jobs |
| API key | None |
| Pagination | pageNumber (0-based), 10 jobs per page, once per locale |
| Full description | Job page HTML (itemprop="description") |
| Salary field | No |
| Remote | No flag; some sites show a free-text "Workplace Type" on the job page |
Finding the Company Subdomain
SuccessFactors Career Site Builder sites live at https://{company}.jobs.hr.cloud.sap, so a careers site at acme.jobs.hr.cloud.sap has the identifier acme. Job pages look like https://acme.jobs.hr.cloud.sap/job/Equipment-Operator/12734-en_US/, where 12734 is the job ID and en_US the locale. Some sites put a brand segment in front, as in /acme-field/job/....
Not every Career Site Builder site exposes the JSON endpoint below. When it isn't there, use the sitemap or search-page fallback.
Locales Split the Results
locale isn't a display setting. Each locale can hold a different set of jobs, not translations of the same ones: on one Danish site, da_DK returned 23 jobs and en_US returned 4 different ones. Leaving locale out returns no jobs at all.
To find the locales a site uses, load its search page and collect the locale=xx_XX values from the language switcher:
curl -s "https://acme.jobs.hr.cloud.sap/search/?searchResultView=LIST" \
| grep -o 'locale=[a-zA-Z]\{2\}_[a-zA-Z]\{2\}' | sort -uIf you find none, en_US is a reasonable single guess. Query every locale you find and merge the results by job id.
The List Request
curl -X POST "https://acme.jobs.hr.cloud.sap/services/recruiting/v1/jobs" \
-H "content-type: application/json" \
-d '{"locale":"en_US","pageNumber":0}'The career site's own request also sends empty keywords, location, facetFilters and similar fields. The minimal body above worked on the site we tested; if a site rejects it, send those fields as empty values too. A trimmed response:
{
"totalJobs": 1110,
"jobSearchResult": [
{
"response": {
"id": "12734",
"unifiedStandardTitle": "Equipment Operator",
"urlTitle": "Equipment-Operator",
"unifiedUrlTitle": "Equipment-Operator",
"unifiedStandardStart": "7/23/26",
"jobLocationShort": ["Ashland, WI "]
}
}
]
}| Field | What it is |
|---|---|
id | Job ID, also the number in the job page URL |
unifiedStandardTitle | Job title |
urlTitle / unifiedUrlTitle | URL slug for the job page |
unifiedStandardStart | Posting start date, formatted for the requested locale |
jobLocationShort | Array of free-text locations, often with trailing spaces |
totalJobs | Total reported for the query |
Build the job page URL as https://{host}/job/{urlTitle}/{id}-{locale}/.
Pagination
pageNumber starts at 0 and each page holds 10 jobs. Keep requesting pages until one comes back empty, and dedupe by id as you go.
Don't stop as soon as your count reaches totalJobs. The endpoint has no stable sort order, so page contents can shift slightly between requests, and stopping early dropped jobs in our tests. Two more end-of-list signals to handle:
- A page past the end returns
{"totalJobs": 1110}with nojobSearchResultkey at all, not an empty array. A locale with no jobs looks the same. - On some sites, a page past the end returns a 500 instead. If you already have results for that locale, treat it as the end of the list.
Here is the list phase in Python:
import re
import requests
def get_locales(host: str) -> list[str]:
html = requests.get(f"https://{host}/search/?searchResultView=LIST", timeout=20).text
return sorted(set(re.findall(r"locale=([a-zA-Z]{2}_[a-zA-Z]{2})", html))) or ["en_US"]
def list_successfactors_jobs(company: str) -> list[dict]:
host = f"{company}.jobs.hr.cloud.sap"
jobs = {}
for locale in get_locales(host):
page = 0
while True:
resp = requests.post(
f"https://{host}/services/recruiting/v1/jobs",
json={"locale": locale, "pageNumber": page},
timeout=20,
)
if resp.status_code >= 400:
if page == 0:
raise RuntimeError("No JSON endpoint here; use sitemap.xml instead")
break # an error after the first page means you're past the end
results = resp.json().get("jobSearchResult") or []
if not results:
break
for item in results:
r = item["response"]
slug = r.get("urlTitle") or r.get("unifiedUrlTitle")
jobs[r["id"]] = {
"id": r["id"],
"title": r.get("unifiedStandardTitle"),
"location": (r.get("jobLocationShort") or [""])[0].strip(),
"start_raw": r.get("unifiedStandardStart"),
"locale": locale,
"url": f"https://{host}/job/{slug}/{r['id']}-{locale}/",
}
page += 1
return list(jobs.values())The Job Page
The list has no description, so fetch each job's URL and parse the HTML. Career Site Builder templates are configurable per company, so the markup differs between sites:
- Description. Many templates mark it with
itemprop="description". On some templates that attribute appears several times, with the first one empty and the text split across the rest, so join every non-empty match instead of taking the first. - Title.
itemprop="title"when present, otherwise theog:titlemeta tag. - Location. Templates with schema.org microdata have one or more
itemprop="jobLocation"blocks, withaddressLocality,addressRegionandaddressCountryincontentattributes. A job can list several. - Date.
itemprop="datePosted"when the template includes microdata. - Labeled fields. Many templates show fields such as "Job Location (Short)", "Req Id", "Employment Type" or "Posting Start Date" as
.joblayouttoken-labelelements followed by a value. The labels are set per company and translated per locale (Danish sites say "Arbejdssted" for location), so match them loosely. - Canonical URL. Read
link rel="canonical". On some sites the job ID in the canonical URL differs from the one in the sitemap URL for the same posting, and the canonical one is the one to store.
from bs4 import BeautifulSoup
def parse_job_page(html: str) -> dict | None:
if "Sorry, this position has been filled." in html:
return None # the posting is closed even though the page returned 200
soup = BeautifulSoup(html, "html.parser")
parts = [el.decode_contents() for el in soup.select('[itemprop="description"]')]
title_el = soup.select_one('[itemprop="title"]')
og_title = soup.find("meta", property="og:title")
canonical = soup.find("link", rel="canonical")
return {
"title": title_el.get_text(strip=True) if title_el else (og_title or {}).get("content"),
"description_html": "".join(p for p in parts if p.strip()),
"locations": [
{
"city": (a.select_one('[itemprop="addressLocality"]') or {}).get("content"),
"region": (a.select_one('[itemprop="addressRegion"]') or {}).get("content"),
"country": (a.select_one('[itemprop="addressCountry"]') or {}).get("content"),
}
for a in soup.select('[itemprop="jobLocation"] [itemprop="address"]')
],
"date_posted": (soup.select_one('[itemprop="datePosted"]') or {}).get("content"),
"canonical_url": canonical["href"] if canonical else None,
}Some templates have no microdata at all. On those you have to find the description by page structure, and that needs checking against each template you meet.
When the JSON Endpoint Isn't There
Two fallbacks give you the job URLs, and then you parse each job page as above:
/sitemap.xml. A flat list of job URLs inloc, each with alastmoddate. One request covers the whole site.lastmodis a last-modified date with day precision, not a posting date./search?startrow=N. The server-rendered search page, 25 job links per page. Stepstartrowby 25 and stop when a page adds no new/job/links. A 4xx here can just mean the site has no jobs or no search page.
Gotchas
Dates follow the locale. unifiedStandardStart is formatted the way that locale displays dates: 7/23/26 for en_US, 20.08.2026 for da_DK, 19.06.26 for de_DE, 29/07/2026 for fr_FR, 2026-05-29 for sv_SE and 2026/08/30 for ja_JP. Parse it with the locale's day, month and year order (Intl.DateTimeFormat or Babel can tell you the order) rather than one fixed format.
Labeled dates are ambiguous too. "Posting Start Date" on US sites is M/D/YY, while some UK sites use day-first DD/MM/YYYY. When both numbers are 12 or less, the "Posting End Date" on the same page can settle it, because a job can't end before it starts.
Don't mistake the end date for the start date. A loose label match on "date" also matches "Posting End Date", which often comes after the start date on the page and silently replaces it. Exclude it explicitly.
Closed jobs return 200. A filled position still answers with a normal page whose body says "Sorry, this position has been filled." Check for that text before you parse.
Location is free text. jobLocationShort has values like "Ashland, WI " with trailing whitespace and no country. Use the page's microdata when it exists, then resolve the rest with a gazetteer; see the location normalization guide.
No salary and no remote flag. Neither the list nor the job page has a pay field we read. "Workplace Type", where a site shows it, is free text such as "Office" or "Field", so check it along with the title and location for "remote" or "hybrid".
Career Site Endpoint vs SAP's Official API
SAP SuccessFactors has authenticated OData APIs for its customers. They need credentials from the employer's own SuccessFactors instance and cover recruiting data well beyond the public listings. The endpoint in this article is the one the public career site itself calls. It needs no account and only returns published jobs, but it's undocumented, it isn't enabled on every site, and its behavior can change.
When This Is Enough
For one company on a jobs.hr.cloud.sap site, the list endpoint plus a job-page parser for that company's template is a reasonable amount of work.
When It Isn't
Across many SuccessFactors employers you end up handling several page templates, many locales and six date formats, before you add other platforms like Workday and Taleo. See which ATS have a public job postings API for how the platforms compare.
CleanJobData reads SuccessFactors career sites alongside other platforms and returns one schema, with dates resolved, locations matched to cities, states and countries, and a remote type. Try it in the Playground or see the API Documentation.
Skip the scraper: get this data from the CleanJobData API
If you'd rather not maintain a SuccessFactors 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.
curl "https://api.cleanjobdata.com/jobs?title=engineer&domain=acme.jobs.hr.cloud.sap&limit=20" \
-H "Authorization: Bearer $CLEANJOBDATA_API_KEY"The domain filter is an exact match on the host of each job's application_url. Every SuccessFactors company has its own jobs.hr.cloud.sap subdomain, so this scopes the query to one employer; 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 for every filter and response field
- Playground to run queries without writing code
- Pricing — the Trial is free with no card required
FAQ
Does SAP SuccessFactors have a public jobs API?
Not a documented one, but many Career Site Builder sites answer a keyless POST to https://{company}.jobs.hr.cloud.sap/services/recruiting/v1/jobs with paged JSON. Sites that don't can still be read from sitemap.xml or the /search?startrow=N pages.
Why does the SuccessFactors API return fewer jobs than the careers site shows?
Results are split by locale, and each locale can hold different jobs. Query every locale listed in the site's language switcher, and merge the results by job ID.
How do I get the job description from SuccessFactors?
Fetch each job's page and read the HTML. Many templates mark the text with itemprop="description", sometimes several times with the first one empty, so join every non-empty match.
Does SuccessFactors include salary in its job data?
Not in any field we read. If an employer publishes pay, it's inside the description text.