Back to articles

ATS Integration Patterns for Clean Job Data

CleanJobData Engineering

Every ATS (Applicant Tracking System — the software an employer uses to post and manage job listings, e.g. Greenhouse, Lever, Workday) exposes hiring data in a different shape. One platform returns structured departments, another stores compensation in description text, and another exposes only a hosted job-board page. CleanJobData hides those differences behind one API contract: list jobs, filter them, and render them without writing a parser for every source.

What the API Hides

The backend does not expect every source to provide the same fields. Each ATS adapter maps its native response into the job shape used by the database and public API. Required fields (always present) vs. optional fields (present when the source provided the data, null or an empty array otherwise):

Always present: title (string), application_url (string), published (ISO timestamp), has_remote (boolean), locations (array — always present, may be empty), experience_levels (array — always present, may be empty).

Optional, null when the source didn't provide it: location (a display string, not structured), description, employment_type, salary_min, salary_max, salary_currency, salary_text, company (an object when the source supplied team/company data, null otherwise), experience_level (a single best-guess derived from experience_levels — see below).

That shape is enforced before data is written to Postgres. Required fields such as title and application_url are validated, optional values are coerced to null, and arrays such as experience_levels are kept stable even when a source is silent.

Source Adapters

CleanJobData uses source-specific adapters instead of one generic scraper. That matters because each ATS has its own API contract and quirks.

  • Ashby — structured postings, secondary locations, and sometimes compensation. Normalization preserves multiple locations, parses structured salary ranges, and keeps posting metadata.
  • Lever — JSON postings plus detailed job content. Normalization resolves departments into employment context and parses locations and descriptions.
  • Greenhouse — public board API with rich content. Normalization maps jobs, departments, locations, and first-published dates into the common schema.
  • Workable — widget API with shortcode-based jobs. Normalization extracts active postings, board context, and location metadata.
  • SmartRecruiters — posting API with detail pages. Normalization fetches details when the list response is incomplete and normalizes released dates.

The adapter layer is where source-specific concepts are translated into JobsDataAPI concepts. For example, a Greenhouse first_published date becomes published, a Lever department can help infer employment context, and an Ashby secondaryLocations list becomes multiple normalized location rows.

Normalization Pipeline, Briefly

Every raw job passes through adapter mapping, then a structural-enforcement stage — trims and validates titles/URLs, coerces dates, parses salary into numbers, derives experience_level from the title — then location resolution against the geo database, then dedup keyed on application_url with is_active/expired_at tracking for postings that disappear from a source. For the full walkthrough of how each stage works and why it's built that way, see How We Normalized Hiring Data from Multiple ATS Platforms — this article stays focused on what that pipeline means for you as an API consumer: the fields you can rely on and the filters they power.

The one stage worth calling out here specifically, because it's what most filters depend on: raw locations are rarely enough on their own. A job may say SF, San Francisco, CA, Remote - US, or London / New York. Location resolution turns that into a locations array with real city_id/state_id/country_id values, which is what makes a filter like this reliable instead of a string match:

curl "https://api.cleanjobdata.com/jobs?city_id=123&remote=true" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"

Querying Normalized Jobs

The public list endpoint accepts the normalized fields developers care about:

curl "https://api.cleanjobdata.com/jobs?title=frontend%20engineer&experience_level=SE&remote=true&salary=120000,180000&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"

Useful filters include:

  • title or search
  • city_id, state_id, country_id, or location=US
  • remote=true, remote_only=true, or has_remote=true
  • experience_level=EN,MI,SE,EX (EN = Entry, MI = Mid, SE = Senior, EX = Executive)
  • salary=120000,180000
  • published_after=2026-06-01
  • max_age=7d
  • company_website_url=company.com
  • sort_by=published or sort_by=relevance

The response includes data, cursor pagination, and meta.filters_applied so your UI can echo the exact filters that were applied.

What Consumers Should Not Do

A clean API is designed to remove integration work. Consumers should not need to:

  • parse ATS-specific JSON responses
  • scrape employer career sites directly
  • guess salary values from text
  • fuzzy-match NYC, New York, and New York, NY
  • write title heuristics for seniority levels
  • maintain separate schemas for each ATS

CleanJobData normalizes those details once, then exposes a stable API that can power job boards, search products, analytics dashboards, and AI hiring tools.