JazzHR Jobs API: Export JazzHR Job Listings (XML Feed)

CleanJobData Engineering

TL;DR

JazzHR publishes a keyless XML export at app.jazz.co/feeds/export/jobs/{company} that lists every open job with title, location, full HTML description, employment type and experience level in one request. The feed has no posting date; that comes from the schema.org JobPosting JSON-LD on each job page at {company}.applytojob.com. There's no salary field in the feed, and some accounts disable the export, in which case you read the career page and its JSON-LD instead.

On this page

To get a company's jobs from JazzHR, request https://app.jazz.co/feeds/export/jobs/{company}: it returns every open job as XML, with full descriptions, and needs no API key. It isn't JSON, but it's one request per company and converts to JSON in a few lines of code.

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 JazzHR 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

EndpointGET https://app.jazz.co/feeds/export/jobs/{company} (XML)
API keyNone
PaginationNone: one response holds every job
Full descriptionYes, in the feed
Salary fieldNo; occasionally baseSalary in the job page's JSON-LD
RemoteNo flag; infer from title or location text

Finding the Company Name

JazzHR career sites live at https://{company}.applytojob.com/apply, and the subdomain is the value the feed wants. For a career site at acme.applytojob.com, the feed URL is:

https://app.jazz.co/feeds/export/jobs/acme

Note that the feed lives on JazzHR's shared app.jazz.co host, not on the company's applytojob.com subdomain. If a company embeds its JazzHR jobs on its own site, look for links to {company}.applytojob.com in the page or the Network tab.

The Feed Request

curl -H "accept: application/xml" "https://app.jazz.co/feeds/export/jobs/acme"

Each open job is a <job> element, and every value is wrapped in CDATA:

<jobs>
  <job>
    <id><![CDATA[job_20260720175631_NWOCXGKSJ09O7H5V]]></id>
    <internalcode><![CDATA[972]]></internalcode>
    <status><![CDATA[Open]]></status>
    <title><![CDATA[Front Desk Coordinator]]></title>
    <department><![CDATA[Operations]]></department>
    <url><![CDATA[http://acme.applytojob.com/apply/J0M3IsBoE4/Front-Desk-Coordinator]]></url>
    <city><![CDATA[New York]]></city>
    <state><![CDATA[NY]]></state>
    <country><![CDATA[United States]]></country>
    <postalcode><![CDATA[]]></postalcode>
    <description><![CDATA[<b>Position:</b> Front Desk Coordinator<br><b>Pay Range:</b>&#160;$20.00 hourly USD ...]]></description>
    <type><![CDATA[Full Time]]></type>
    <experience><![CDATA[Entry Level]]></experience>
  </job>
</jobs>

Feed Fields

FieldWhat it holds
idJazzHR's job ID, e.g. job_20260720175631_...
statusKeep only Open
titleJob title
departmentDepartment label, if set
urlPublic job page on applytojob.com
city, stateCity, and usually a state abbreviation
countryA full country name, such as United States
postalcodeUsually empty
descriptionFull HTML description
typeEmployment type, such as Full Time
experienceExperience level, such as Entry Level

The feed also has fields you can ignore, such as buttons, which holds an apply-widget script snippet.

Converting the Feed to JSON (Python)

import json
import re
import requests
import xml.etree.ElementTree as ET
 
def fetch_jazzhr_jobs(company: str) -> list:
    r = requests.get(
        f"https://app.jazz.co/feeds/export/jobs/{company}",
        headers={"accept": "application/xml"},
        timeout=20,
    )
    r.raise_for_status()
    root = ET.fromstring(r.content)
    fields = ["id", "status", "title", "department", "url", "city",
              "state", "country", "description", "type", "experience"]
    jobs = []
    for job in root.iter("job"):
        row = {f: (job.findtext(f) or "").strip() for f in fields}
        if row["status"] and row["status"].lower() != "open":
            continue
        jobs.append(row)
    return jobs
 
JSONLD_RE = re.compile(r'<script[^>]+type="application/ld\+json"[^>]*>(.*?)</script>', re.S)
 
def fetch_date_posted(job_url: str):
    """The feed has no date; the job page's JobPosting JSON-LD does."""
    html = requests.get(job_url.replace("http://", "https://", 1), timeout=20).text
    for block in JSONLD_RE.findall(html):
        try:
            data = json.loads(block)
        except ValueError:
            continue
        if data.get("@type") == "JobPosting":
            return data.get("datePosted")
    return None

ElementTree unwraps the CDATA sections for you, so description comes back as an HTML string ready to sanitize and store.

Getting the Posting Date

The feed has no date field at all. If you need to know when a job was posted, fetch the job's url and read the schema.org JobPosting block in its JSON-LD. Its datePosted is a plain date such as 2026-07-22, and the block also has validThrough, employmentType (such as FULL_TIME) and experienceRequirements. See our JobPosting schema guide for the field reference.

That costs one page request per job, so only do it for jobs you haven't seen before. Store the date the first time and skip the page for known IDs on later runs.

When the Feed Is Disabled

The XML export is a per-account setting, and some accounts turn it off even though their career site is live. The feed then answers with a 404 or 403. In that case:

  1. Fetch https://{company}.applytojob.com/apply and collect the job links, which look like /apply/{jobId}/{slug}. The same link often appears twice per job, so dedupe by jobId.
  2. Fetch each job page and read its JobPosting JSON-LD for the title, description, location, date and employment type.

This route costs a request per job, but the JSON-LD has everything the feed has plus the date.

Gotchas

IDs differ between the feed and the URL. The feed's id looks like job_20260720175631_..., while the job URL contains a short token such as J0M3IsBoE4. Pick one as your dedup key and stick with it. The job page's JSON-LD carries the feed-style ID as uniqueJobCode, which lets you match the two.

Country is a full name. country is United States, not US, so you need a name-to-code lookup. Some jobs have no city or state and only a country, and some use a placeholder such as Multiple Countries, which isn't a place. The location normalization guide covers these cases.

Descriptions contain HTML entities. Expect &#160; and inline styles inside the CDATA. Decode entities and sanitize the HTML before you render it.

There's no salary field. Pay, when an employer shares it, usually sits in the description text as something like "Pay Range: $20.00 hourly USD". The job page's JSON-LD can carry a structured baseSalary, but it's rare on JazzHR.

No remote flag. Look for "remote" or "hybrid" in the title and location text, and accept that some remote jobs only say so in the description.

Job URLs may use http://. Upgrade them to https:// before you fetch or display them.

Edits are easy to miss. If you only fetch job pages for new IDs, you won't notice when an employer edits a job. The feed itself is cheap, so re-read titles and descriptions from it on every run.

Public Feed vs JazzHR's Official API

JazzHR also has an official API for its customers. It needs an API key from the employer's own JazzHR account and covers that company's hiring data, such as applicants. You don't need it for published job ads: the export feed above is public and returns what's on the company's career site.

When This Is Enough

For one company's jobs, the feed is about as easy as it gets: one request, every open job, full descriptions. Add the job-page lookup if you need dates.

When It Isn't

Across many companies and platforms you end up with a collector per ATS, each with its own format, date source and location quirks. The ATS public job postings API list shows how they compare.

CleanJobData reads JazzHR alongside other platforms and returns one schema, with locations resolved to cities, states and countries, a remote type, and salary parsed from the description when the employer includes it. 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 JazzHR 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=coordinator&domain=acme.applytojob.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. JazzHR jobs apply on each company's own applytojob.com subdomain, so this scopes the query to one employer; use company_website_url or employer_id as alternatives. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

FAQ

Does JazzHR have a public jobs API?

JazzHR has a public XML export rather than a JSON API. https://app.jazz.co/feeds/export/jobs/{company} returns every open job for an account with full descriptions, and it needs no API key.

How do I find a company's JazzHR feed URL?

Take the subdomain from the company's career site at {company}.applytojob.com and put it at the end of https://app.jazz.co/feeds/export/jobs/. For acme.applytojob.com, the feed is https://app.jazz.co/feeds/export/jobs/acme.

Does the JazzHR feed include the posting date?

No. The feed has no date field. Fetch each job's page and read datePosted from its schema.org JobPosting JSON-LD, and store it the first time you see the job.

What if the JazzHR feed returns 404?

The account has probably disabled the export. Read the career page at {company}.applytojob.com/apply for job links instead, then parse the JSON-LD on each job page.