All platforms

HiringThing Jobs API.

Pull an entire company board in one request: every HiringThing subdomain publishes an RSS feed carrying full HTML descriptions, locations, and posted dates.

Get API access
HiringThing
Live
15K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using HiringThing
Demco, Inc.Walker Art Center
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 HiringThing.

Data fields
  • Full HTML Descriptions
  • Structured Pay Ranges
  • City & State Locations
  • Posted Dates
  • Remote vs On-Site Flags
  • Job Category Tags
Use cases
  1. 01SMB Job Aggregation
  2. 02Career Page Monitoring
  3. 03Location-Based Filtering
  4. 04Salary Benchmarking
Trusted by
Demco, Inc.Walker Art Center
DIY GUIDE

How to scrape HiringThing.

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

HybridbeginnerNo documented limits (use standard delays)No auth

Fetch the RSS feed

Every HiringThing company subdomain publishes an RSS feed at /api/rss.xml that returns every open job with full HTML descriptions. It is the fastest way to pull an entire board in a single request.

Step 1: Fetch the RSS feed
import requests
import xml.etree.ElementTree as ET

company = "demco"
rss_url = f"https://{company}.hiringthing.com/api/rss.xml"

response = requests.get(rss_url)
response.raise_for_status()

root = ET.fromstring(response.content)
channel = root.find("channel")
items = channel.findall("item")

print(f"Found {len(items)} jobs in RSS feed")

Parse job entries from the feed

Walk each RSS <item> to read the title, link, location, and the full HTML body carried in the media:description tag. The job ID is the numeric segment of the link path (/job/{id}/...).

Step 2: Parse job entries from the feed
import xml.etree.ElementTree as ET

def parse_rss_jobs(rss_content):
    root = ET.fromstring(rss_content)
    channel = root.find("channel")
    jobs = []

    for item in channel.findall("item"):
        title_elem = item.find("title")
        link_elem = item.find("link")
        location_elem = item.find("location")
        description_elem = item.find(".//{http://search.yahoo.com/mrss/}description")

        jobs.append({
            "title": title_elem.text if title_elem is not None else None,
            "url": link_elem.text if link_elem is not None else None,
            "location": location_elem.text if location_elem is not None else None,
            "description_html": description_elem.text if description_elem is not None else None,
        })

    return jobs

jobs = parse_rss_jobs(response.content)
for job in jobs[:3]:
    print(f"{job['title']} - {job['location']}")

Decode embedded JSON on job pages

For metadata the feed omits — company ID, structured location, and posted date — fetch a job page and decode the data-react-props JSON on the ApplyButtonGroup React component.

Step 3: Decode embedded JSON on job pages
import requests
import json
import re
from bs4 import BeautifulSoup

def get_job_details(company: str, job_url: str) -> dict:
    full_url = f"https://{company}.hiringthing.com{job_url}" if job_url.startswith("/") else job_url
    response = requests.get(full_url)
    soup = BeautifulSoup(response.text, "html.parser")

    # Find the ApplyButtonGroup component with embedded JSON
    apply_div = soup.find("div", {"data-react-class": "HiringThing.Components.ApplyButtonGroup"})
    if not apply_div:
        return {}

    props_json = apply_div.get("data-react-props", "{}")
    data = json.loads(props_json)
    job_data = data.get("jobObj", {}).get("table", {})

    return {
        "id": job_data.get("id"),
        "title": job_data.get("title"),
        "location": job_data.get("location"),
        "location_info": job_data.get("location_info"),
        "description_html": job_data.get("html_description"),
        "posted_at": job_data.get("posted_at"),
        "company_id": job_data.get("company_id"),
    }

details = get_job_details("demco", "/job/984299/learning-environment-field-consultant")
print(details)

Read salary data from React props

Pay ranges live in a separate JobSalary React component rather than the feed. Read min, max, currency, and pay frequency from its data-react-props attribute on the listings page.

Step 4: Read salary data from React props
import requests
import json
from bs4 import BeautifulSoup

def extract_salary_data(html_content: str) -> list:
    soup = BeautifulSoup(html_content, "html.parser")
    salaries = []

    salary_divs = soup.find_all("div", {"data-react-class": "HiringThing.Components.JobSalary"})

    for div in salary_divs:
        props = div.get("data-react-props", "{}")
        data = json.loads(props)

        salaries.append({
            "min": data.get("minSalary", {}).get("amount"),
            "max": data.get("maxSalary", {}).get("amount"),
            "currency": data.get("minSalary", {}).get("currency"),
            "frequency": data.get("payFrequency"),
        })

    return salaries

# Fetch listings page
response = requests.get("https://demco.hiringthing.com/")
salaries = extract_salary_data(response.text)

for s in salaries[:3]:
  print(f"${s['min']} - ${s['max']} {s['frequency']}")

Handle errors and pace requests

Map a 404 to a missing company and back off on 401, 403, or 429 responses. When sweeping many boards, add a short delay between companies to stay polite.

Step 5: Handle errors and pace requests
import requests
import time
import xml.etree.ElementTree as ET

def scrape_hiringthing_company(company: str, delay: float = 0.5) -> dict:
    rss_url = f"https://{company}.hiringthing.com/api/rss.xml"

    try:
        response = requests.get(rss_url, timeout=30)

        if response.status_code == 404:
            return {"error": f"Company '{company}' not found", "jobs": []}

        response.raise_for_status()

        root = ET.fromstring(response.content)
        channel = root.find("channel")
        items = channel.findall("item")

        jobs = []
        for item in items:
            jobs.append({
                "title": item.find("title").text,
                "url": item.find("link").text,
                "location": getattr(item.find("location"), "text", None),
            })

        return {"company": company, "jobs": jobs, "count": len(jobs)}

    except requests.RequestException as e:
        return {"error": str(e), "jobs": []}

# Scrape multiple companies with rate limiting
companies = ["demco", "example1", "example2"]
for company in companies:
    result = scrape_hiringthing_company(company)
    print(f"{company}: {result.get('count', 0)} jobs")
    time.sleep(delay)
Common issues
mediumRSS feed returns 404 for some companies

A 404 means the board or subdomain does not exist. Confirm the company slug from the careers link, and fall back to HTML scraping of the listings page (div.job-container) if the feed is disabled.

lowEmbedded JSON uses Unicode escapes

The data-react-props attribute escapes markup (e.g. \u003c for <). Python's json.loads() unescapes this automatically, so parse before inspecting the raw string.

highThe /api/v1/jobs endpoint returns 401

That endpoint is an authenticated admin API gated by a signed _ats_session cookie, not a public source. Use the RSS feed or embedded JSON instead — do not try to forge the session.

lowSalary is missing from the RSS feed

The feed omits compensation. Fetch the HTML listings page and parse the data-react-props on JobSalary components for min, max, currency, and pay frequency.

Best practices
  1. 1Use the RSS feed for the fastest pull — one request returns all jobs with full descriptions
  2. 2Read data-react-props attributes for structured salary, location, and posted-date metadata
  3. 3Add a 0.5-1 second delay between company boards when scraping in bulk
  4. 4Cache results by company ID (present in the embedded JSON and sitemaps) to dedupe across refreshes
  5. 5Fall back to HTML scraping of div.job-container when a feed is unavailable
  6. 6Discover all boards through the global S3 sitemap index (gzipped per-company sitemaps)
Or skip the complexity

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

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

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