All platforms

iCIMS Jobs API.

Pull structured job data — titles, full descriptions, salary bands, employment type, and locations — from the enterprise career sites hosted on this platform, no public API required.

Get API access
iCIMS
Live
350K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using iCIMS
Emory UniversityLatham & WatkinsGranicusAM BestBridge Core
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 iCIMS.

Data fields
  • Full Job Descriptions
  • Structured Salary Bands
  • Employment Type
  • City, State & Country Locations
  • Posted & Closing Dates
  • Apply URLs
Use cases
  1. 01Enterprise Job Aggregation
  2. 02Salary Benchmarking
  3. 03Large-Employer Job Tracking
  4. 04Recruitment Market Research
Trusted by
Emory UniversityLatham & WatkinsGranicusAM BestBridge Core
DIY GUIDE

How to scrape iCIMS.

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

HTMLadvanced~500ms between requests; cap concurrent detail fetches at 3No auth

Discover jobs via sitemap.xml

The most efficient way to enumerate every open role is the sitemap.xml file: it lists all job URLs with lastmod timestamps in a single request, so you avoid paging through the search HTML.

Step 1: Discover jobs via sitemap.xml
import requests
import xml.etree.ElementTree as ET

company = "careers-bcore"
sitemap_url = f"https://{company}.icims.com/sitemap.xml"

response = requests.get(sitemap_url, timeout=30)
response.raise_for_status()

root = ET.fromstring(response.content)
namespaces = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}

job_urls = []
for url in root.findall('ns:url', namespaces):
    loc = url.find('ns:loc', namespaces)
    lastmod = url.find('ns:lastmod', namespaces)
    if loc is not None and '/jobs/' in loc.text:
        job_urls.append({
            'url': loc.text,
            'lastmod': lastmod.text if lastmod is not None else None
        })

print(f"Found {len(job_urls)} job URLs in sitemap")

Parse job listings from the search page

When the sitemap is unavailable, fetch the server-rendered search page. v1 tenants use /jobs/search; v2 tenants (host prefix internal-) inline their anchors under /jobs/search?json=true. Add in_iframe=1 to strip the wrapper chrome.

Step 2: Parse job listings from the search page
import requests
from bs4 import BeautifulSoup
import re

company = "careers-bcore"
listings_url = f"https://{company}.icims.com/jobs/search?in_iframe=1"

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html',
}

response = requests.get(listings_url, headers=headers, timeout=30)
soup = BeautifulSoup(response.content, 'html.parser')

jobs = []
for link in soup.select('a.iCIMS_Anchor[href*="/jobs/"]'):
    href = link.get('href', '')
    title = link.get('title') or link.get_text(strip=True)

    # Extract job ID from URL pattern /jobs/{id}/
    job_id_match = re.search(r'/jobs/(\d+)/', href)
    job_id = job_id_match.group(1) if job_id_match else None

    # Skip login / apply / referral links
    if job_id and title and not any(x in href.lower() for x in ('/login', 'mode=apply', '/referral')):
        jobs.append({
            'id': job_id,
            'title': title,
            'url': href
        })

print(f"Found {len(jobs)} jobs on listing page")

Extract job details from JSON-LD (with HTML fallback)

iCIMS detail pages embed a JobPosting JSON-LD block — the cleanest source for title, description, salary, employment type, and dates. When a tenant leaves the JSON-LD empty, fall back to the iCIMS_ HTML sections.

Step 3: Extract job details from JSON-LD (with HTML fallback)
import requests
import json
from bs4 import BeautifulSoup

job_url = "https://careers-bcore.icims.com/jobs/2931/applications-developer/job"
detail_url = f"{job_url}?in_iframe=1"

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
}

response = requests.get(detail_url, headers=headers, timeout=30)
soup = BeautifulSoup(response.content, 'html.parser')

job = {}

# Preferred path: parse the embedded JobPosting JSON-LD
for script in soup.find_all('script', type='application/ld+json'):
    try:
        data = json.loads(script.string or '')
    except (json.JSONDecodeError, TypeError):
        continue
    if isinstance(data, dict) and data.get('@type') == 'JobPosting':
        job['title'] = data.get('title')
        job['description'] = data.get('description')
        job['employment_type'] = data.get('employmentType')
        job['posted_at'] = data.get('datePosted')
        job['closes_at'] = data.get('validThrough')
        job['apply_url'] = data.get('url')
        job['company'] = (data.get('hiringOrganization') or {}).get('name')
        salary_value = (data.get('baseSalary') or {}).get('value') or {}
        job['salary_min'] = salary_value.get('minValue')
        job['salary_max'] = salary_value.get('maxValue')
        break

# Fallback: parse server-rendered HTML when JSON-LD is absent or blank
if not job.get('title'):
    heading = soup.select_one('h1.iCIMS_Header') or soup.select_one('h1')
    job['title'] = heading.get_text(strip=True) if heading else None

if not job.get('description'):
    sections = [s.get_text('\n', strip=True)
                for s in soup.select('.iCIMS_InfoMsg_Job .iCIMS_Expandable_Text')]
    job['description'] = '\n'.join(filter(None, sections)) or None

if not job.get('apply_url'):
    apply_link = (soup.select_one('a[href*="mode=apply"]')
                  or soup.select_one('a.iCIMS_ApplyOnlineButton'))
    job['apply_url'] = apply_link.get('href') if apply_link else None

print(job)

Handle pagination for HTML scraping

Many tenants list every role on one search page, but larger boards page with the 'pr' (page result) parameter starting at 0. Terminate defensively when a page returns no new job anchors.

Step 4: Handle pagination for HTML scraping
import requests
from bs4 import BeautifulSoup
import re
import time

company = "careers-bcore"
all_jobs = []
seen = set()
page = 0

while True:
    url = f"https://{company}.icims.com/jobs/search?pr={page}&in_iframe=1"

    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    }

    response = requests.get(url, headers=headers, timeout=30)
    soup = BeautifulSoup(response.content, 'html.parser')

    job_links = soup.select('a.iCIMS_Anchor[href*="/jobs/"]')

    new_count = 0
    for link in job_links:
        href = link.get('href', '')
        title = link.get('title') or link.get_text(strip=True)
        job_id_match = re.search(r'/jobs/(\d+)/', href)

        if job_id_match and job_id_match.group(1) not in seen:
            seen.add(job_id_match.group(1))
            new_count += 1
            all_jobs.append({
                'id': job_id_match.group(1),
                'title': title,
                'url': href
            })

    # Stop when a page adds no new records (defensive termination)
    if new_count == 0:
        break

    print(f"Page {page}: +{new_count} jobs (total: {len(all_jobs)})")
    page += 1

    # Be respectful - add delay between requests
    time.sleep(1)

    # Safety limit
    if page > 100:
        break

print(f"Total jobs found: {len(all_jobs)}")

Check robots.txt for the sitemap and disallowed paths

iCIMS robots.txt advertises the sitemap location and disallows candidate/login/referral paths. Read it first to find the sitemap and respect the restrictions.

Step 5: Check robots.txt for the sitemap and disallowed paths
import requests

company = "careers-bcore"
robots_url = f"https://{company}.icims.com/robots.txt"

response = requests.get(robots_url, timeout=30)
robots_content = response.text

# Parse sitemap URL from robots.txt
sitemap_url = None
for line in robots_content.split('\n'):
    if line.lower().startswith('sitemap:'):
        sitemap_url = line.split(':', 1)[1].strip()
        print(f"Sitemap found: {sitemap_url}")
        break

# Note disallowed paths (e.g. /jobs/*login, /jobs/*referral, /jobs/*candidate)
disallowed = []
for line in robots_content.split('\n'):
    if line.lower().startswith('disallow:'):
        path = line.split(':', 1)[1].strip()
        if path:
            disallowed.append(path)

print(f"Disallowed paths: {disallowed}")
Common issues
highNo public JSON jobs API

iCIMS exposes no JSON endpoint for listings; the Jibe /api/jobs path returns server-rendered HTML, not JSON. Fetch the search page (/jobs/search on v1 tenants, /jobs/search?json=true on v2 internal-* tenants) and extract every /jobs/{id}/ anchor from the HTML.

highTenant host shape varies across v1, v2, and Jibe

The same employer can appear as careers-{co}.icims.com, internal-{co}.icims.com (v2, client-rendered), jobs-/jobs2-/staff-{co}.icims.com, or {co}.jibeapply.com. Detect the internal-* prefix to switch listings to ?json=true, strip the role prefix for a canonical tenant slug, and expect v2 detail pages to yield listings only (no server-side description body).

mediumDescription split between JSON-LD and HTML sections

Some tenants populate the JobPosting JSON-LD description; others leave it empty and render the body only in HTML. Prefer the JSON-LD description, and when it is missing or blank, concatenate the .iCIMS_InfoMsg_Job .iCIMS_Expandable_Text sections (falling back to .iCIMS_Expandable_Text).

mediumIframe wrapper requires in_iframe=1

The public career page renders job content inside an iframe, so the bare URL returns the wrapper chrome instead of the job HTML. Append ?in_iframe=1 (or &in_iframe=1) to every listings and detail URL.

mediumLocations use encoded US-XX-City strings and UNAVAILABLE placeholders

HTML labels arrive as US-CA-San Francisco or GB-ENG-London, and JSON-LD address fields sometimes contain the literal 'UNAVAILABLE'. Split the encoded prefix into city/state/country and drop 'UNAVAILABLE' values; JSON-LD jobLocation.address gives cleaner addressLocality/Region/Country when present.

highCloudflare or bot detection returns 403 / 429

Some tenants sit behind Cloudflare and block unheadered or aggressive clients. Send a desktop User-Agent, keep concurrency low (~3 detail fetches with ~500ms spacing), and back off on 403/429.

Best practices
  1. 1Prefer JobPosting JSON-LD on detail pages for title, description, salary, employment type, and dates; fall back to iCIMS_ HTML sections when it is empty
  2. 2Use /sitemap.xml to enumerate every /jobs/ URL in a single request instead of paging the search HTML
  3. 3Always append ?in_iframe=1 so responses contain just the job content, not the wrapper page
  4. 4Detect internal-* (v2) tenants and request /jobs/search?json=true; use /jobs/search for v1 hosts
  5. 5Strip careers-/internal-/jobs-/staff- prefixes to a canonical tenant slug so candidate and internal boards deduplicate together
  6. 6Keep concurrency low (~3) with a desktop User-Agent to avoid Cloudflare blocks and mobile redirects
Or skip the complexity

One endpoint. All iCIMS jobs. No scraping, no sessions, no maintenance.

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

Access iCIMS
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