How to Scrape Ashby Job Listings
Of the major ATS platforms, Ashby's public API returns the most structured location and compensation data by default — a real address object instead of one free-text string, and a compensation array with named tiers instead of a single number pair. That doesn't mean it's problem-free; here's the full shape and where it still breaks down.
Finding the Organization Slug
An Ashby-hosted careers page lives at https://jobs.ashbyhq.com/{slug}. Visit the company's careers page (typically linked from their site's careers/jobs footer) and the slug is the path segment after ashbyhq.com/. As with any ATS, this only tells you the slug for a company whose careers page you already know — there's no public directory mapping every company to its Ashby slug, and finding those at scale across thousands of employers is a separate, much harder problem than fetching one board you already have the address for.
Fetching the Full Posting List
curl "https://api.ashbyhq.com/posting-api/job-board/acme?includeCompensation=true"This is a plain GET request, no authentication, and returns every open posting with full detail in one response — including compensation, when includeCompensation=true is set. Omit that param and compensation fields are stripped from the response even when the posting has them configured.
The Full Response Shape
{
"jobs": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"title": "Senior Backend Engineer",
"locationName": "San Francisco, CA",
"workplaceType": "hybrid",
"isRemote": false,
"employmentType": "FullTime",
"publishedAt": "2026-07-01T12:00:00.000Z",
"jobUrl": "https://jobs.ashbyhq.com/acme/f47ac10b-58cc-4372-a567-0e02b2c3d479",
"descriptionHtml": "<p>We're looking for...</p>",
"address": {
"postalAddress": {
"addressLocality": "San Francisco",
"addressRegion": "California",
"addressCountry": "United States"
}
},
"secondaryLocations": [
{
"location": "Remote - US",
"address": { "postalAddress": { "addressCountry": "United States" } }
}
],
"compensation": {
"compensationTierSummary": "$150K – $180K",
"summaryComponents": [
{
"compensationType": "Salary",
"interval": "1 YEAR",
"minValue": 150000,
"maxValue": 180000,
"currencyCode": "USD"
}
]
}
}
]
}Field-by-field:
id— a UUID. Stable dedup key.locationName— still free text, same inconsistency problem as every other ATS. Use it as a display fallback, not your primary location source here —addressis better.address.postalAddress— an actual structured object:addressLocality(city),addressRegion(state/province),addressCountry(country name, not a code). This is real structured location, not something you have to parse from a sentence — genuinely rare among ATS public APIs. The catch:addressCountryis a full country name ("United States"), not an ISO code, so you still need a name-to-code lookup table if you want ISO codes downstream.secondaryLocations— an array covering multi-location and remote-eligible postings, each with its own nestedaddress. A posting listed as based in one city but open to remote hires elsewhere will show that here — miss this array and you'll only capture the primary office location.workplaceTypeandisRemote— two separate signals, and in practice neither is reliably populated on its own: both are commonlynull/absent on real postings. Don't trust either alone — treat a posting as remote if any ofisRemote === true,workplaceType === "remote"(or"hybrid"for hybrid), or the word "remote" appears inlocationNameholds true, and fall back tolocationNametext as the last resort when the structured fields are both empty.employmentType— a cleaner enum than most ATS platforms (FullTime,PartTime,Contract,Internin practice), but still worth normalizing rather than trusting blindly, since case and exact wording can vary by how each company's Ashby instance is configured.compensation.summaryComponents— the real prize here: an array of structured compensation components, each withcompensationType,interval(a numeric-prefixed string like"1 YEAR"or"1 HOUR", not a bare unit — match it with a substring check rather than exact equality),minValue,maxValue, andcurrencyCode. A posting can have more than one component (e.g. base salary plus a separate equity or bonus tier) — if you only read the first array entry, you'll sometimes grab a bonus tier instead of base salary. Filter bycompensationType === "Salary"(or whatever the equivalent string is in the payload you're looking at) before treating a value as base pay.compensation.compensationTierSummary— a human-readable string version of the same data ("$150K – $180K"). Useful for display, not for filtering or math — parsesummaryComponentsfor that.descriptionHtml— the full posting body as HTML, generally clean.
The Real Pain Points
Compensation components need filtering, not just reading. The richness of summaryComponents is also its trap — grabbing [0] blindly without checking compensationType and interval will occasionally hand you an hourly bonus figure and label it as an annual salary.
Country is a name, not a code, even in the structured field. addressCountry being a full name means you still need normalization logic even for the "good" location data — it's less work than parsing a full free-text string, but it's not zero work.
Not every posting has structured compensation, even with includeCompensation=true set. Disclosure still depends on the employer and, in some regions, local law — expect compensation to be absent or partially filled on a meaningful share of postings.
No webhooks, consistent with every ATS in this category — poll and diff ids to catch closures.
When This Is Enough
If you're pulling from a handful of Ashby-hosted companies, this is the best-structured API of the major ATS platforms for location and compensation — you'll write meaningfully less parsing code than you would for Greenhouse or Workday.
When It Isn't
The structure only helps within Ashby. The moment you're combining Ashby postings with Greenhouse, Lever, or Workday postings in the same product, you're back to reconciling four different response shapes — a UUID here, a sequential ID there, a compensation array here, a single salary pair there — into one schema. That reconciliation, done once across ATS platforms, is what a normalized data API like JobsDataAPI exists to handle.